Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/client/acontext-py/src/acontext/agent/disk.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,11 @@ def execute(self, ctx: DiskContext, llm_arguments: dict) -> str:
expire=expire,
)

if not result.public_url:
raise RuntimeError("Failed to get public URL: server did not return a URL.")
if result.public_url:
return f"Public download URL for '{normalized_path}{filename}' (expires in {expire}s):\n{result.public_url}"

return f"Public download URL for '{normalized_path}{filename}' (expires in {expire}s):\n{result.public_url}"
# Encryption enabled — no presigned URL
return f"File '{normalized_path}{filename}' is available. Use the download API endpoint to retrieve its content."

async def async_execute(self, ctx: AsyncDiskContext, llm_arguments: dict) -> str:
"""Get a public download URL for a file (async)."""
Expand All @@ -478,10 +479,10 @@ async def async_execute(self, ctx: AsyncDiskContext, llm_arguments: dict) -> str
expire=expire,
)

if not result.public_url:
raise RuntimeError("Failed to get public URL: server did not return a URL.")
if result.public_url:
return f"Public download URL for '{normalized_path}{filename}' (expires in {expire}s):\n{result.public_url}"

return f"Public download URL for '{normalized_path}{filename}' (expires in {expire}s):\n{result.public_url}"
return f"File '{normalized_path}{filename}' is available. Use the download API endpoint to retrieve its content."


class GrepArtifactsTool(BaseTool):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ async def download(

if resp.content is not None:
file_dest.write_text(resp.content.raw, encoding="utf-8")
elif resp.raw_content is not None:
import base64
file_dest.write_bytes(base64.b64decode(resp.raw_content))
elif resp.url is not None:
async with httpx.AsyncClient() as http:
r = await http.get(resp.url)
Expand Down
3 changes: 3 additions & 0 deletions src/client/acontext-py/src/acontext/resources/skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ def download(

if resp.content is not None:
file_dest.write_text(resp.content.raw, encoding="utf-8")
elif resp.raw_content is not None:
import base64
file_dest.write_bytes(base64.b64decode(resp.raw_content))
elif resp.url is not None:
r = httpx.get(resp.url)
r.raise_for_status()
Expand Down
8 changes: 8 additions & 0 deletions src/client/acontext-py/src/acontext/types/skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ class GetSkillFileResp(BaseModel):
None,
description="Parsed file content if available (present if file is parseable)",
)
raw_content: str | None = Field(
None,
description="Base64-encoded binary content (present when encryption is enabled for non-text files)",
)
content_mime: str | None = Field(
None,
description="MIME type of raw_content",
)


class DownloadSkillToSandboxResp(BaseModel):
Expand Down
7 changes: 4 additions & 3 deletions src/client/acontext-ts/src/agent/disk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,11 +290,12 @@ export class DownloadFileTool extends AbstractBaseTool {
expire,
});

if (!result.public_url) {
throw new Error('Failed to get public URL: server did not return a URL.');
if (result.public_url) {
return `Public download URL for '${normalizedPath}${filename}' (expires in ${expire}s):\n${result.public_url}`;
}

return `Public download URL for '${normalizedPath}${filename}' (expires in ${expire}s):\n${result.public_url}`;
// Encryption enabled — no presigned URL, content available via API proxy
return `File '${normalizedPath}${filename}' is available. Use the download API endpoint to retrieve its content.`;
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/client/acontext-ts/src/resources/skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ export class SkillsAPI {

if (resp.content) {
await fs.writeFile(fileDest, resp.content.raw, 'utf-8');
} else if (resp.raw_content) {
// Binary content returned as base64 (when encryption is enabled)
const buffer = Buffer.from(resp.raw_content, 'base64');
await fs.writeFile(fileDest, buffer);
} else if (resp.url) {
const r = await fetch(resp.url);
if (!r.ok) {
Expand Down
2 changes: 2 additions & 0 deletions src/client/acontext-ts/src/types/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export const GetSkillFileRespSchema = z.object({
mime: z.string(),
url: z.string().nullable().optional(),
content: FileContentSchema.nullable().optional(),
raw_content: z.string().nullable().optional(), // base64-encoded binary content (when encryption enabled)
content_mime: z.string().nullable().optional(),
});

export type GetSkillFileResp = z.infer<typeof GetSkillFileRespSchema>;
Expand Down
3 changes: 3 additions & 0 deletions src/server/api/go/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/memodb-io/Acontext/internal/bootstrap"
"github.com/memodb-io/Acontext/internal/config"
"github.com/memodb-io/Acontext/internal/infra/cache"
encryptionpkg "github.com/memodb-io/Acontext/internal/infra/crypto"
dbpkg "github.com/memodb-io/Acontext/internal/infra/db"
"github.com/memodb-io/Acontext/internal/modules/handler"
"github.com/memodb-io/Acontext/internal/pkg/tokenizer"
Expand Down Expand Up @@ -93,11 +94,13 @@ func main() {
learningSpaceHandler := do.MustInvoke[*handler.LearningSpaceHandler](inj)
sessionEventHandler := do.MustInvoke[*handler.SessionEventHandler](inj)
projectHandler := do.MustInvoke[*handler.ProjectHandler](inj)
encSvc := do.MustInvoke[*encryptionpkg.EncryptionService](inj)

engine := router.NewRouter(router.RouterDeps{
Config: cfg,
DB: db,
Log: log,
EncryptionService: encSvc,
SessionHandler: sessionHandler,
DiskHandler: diskHandler,
ArtifactHandler: artifactHandler,
Expand Down
4 changes: 4 additions & 0 deletions src/server/api/go/configs/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@ telemetry:

artifact:
maxUploadSizeBytes: ${ARTIFACT_MAX_UPLOAD_SIZE_BYTES} # Default 16MB (16 * 1024 * 1024 bytes)

encryption:
enabled: ${ENCRYPTION_ENABLED} # Set to true to enable S3 envelope encryption
masterKey: "${ENCRYPTION_MASTER_KEY}" # Admin master key (required when enabled)
10 changes: 9 additions & 1 deletion src/server/api/go/internal/bootstrap/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/memodb-io/Acontext/internal/config"
"github.com/memodb-io/Acontext/internal/infra/blob"
"github.com/memodb-io/Acontext/internal/infra/cache"
encryptionpkg "github.com/memodb-io/Acontext/internal/infra/crypto"
"github.com/memodb-io/Acontext/internal/infra/db"
"github.com/memodb-io/Acontext/internal/infra/httpclient"
"github.com/memodb-io/Acontext/internal/infra/logger"
Expand Down Expand Up @@ -185,10 +186,17 @@ func BuildContainer() *do.Injector {
return mq.NewPublisher(conn, log, cfg, dialFn)
})

// Encryption
do.Provide(inj, func(i *do.Injector) (*encryptionpkg.EncryptionService, error) {
cfg := do.MustInvoke[*config.Config](i)
return encryptionpkg.NewEncryptionService(cfg.Encryption.MasterKey, cfg.Encryption.Enabled)
})

// S3
do.Provide(inj, func(i *do.Injector) (*blob.S3Deps, error) {
cfg := do.MustInvoke[*config.Config](i)
return blob.NewS3(context.Background(), cfg)
encSvc := do.MustInvoke[*encryptionpkg.EncryptionService](i)
return blob.NewS3(context.Background(), cfg, encSvc)
})
// get presign expire duration
do.Provide(inj, func(i *do.Injector) (func() time.Duration, error) {
Expand Down
28 changes: 18 additions & 10 deletions src/server/api/go/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,23 @@ type ArtifactCfg struct {
MaxUploadSizeBytes int64 // Maximum file upload size in bytes
}

type EncryptionCfg struct {
MasterKey string // Admin master key for envelope encryption (env: APP_ENCRYPTION_MASTERKEY)
Enabled bool // Enable S3 envelope encryption (default: false)
}

type Config struct {
App AppCfg
Root RootCfg
Log LogCfg
Database DBCfg
Redis RedisCfg
RabbitMQ MQCfg
S3 S3Cfg
Core CoreCfg
Telemetry TelemetryCfg
Artifact ArtifactCfg
App AppCfg
Root RootCfg
Log LogCfg
Database DBCfg
Redis RedisCfg
RabbitMQ MQCfg
S3 S3Cfg
Core CoreCfg
Telemetry TelemetryCfg
Artifact ArtifactCfg
Encryption EncryptionCfg
}

func setDefaults(v *viper.Viper) {
Expand Down Expand Up @@ -132,6 +138,8 @@ func setDefaults(v *viper.Viper) {
v.SetDefault("telemetry.enabled", true)
v.SetDefault("telemetry.sampleRatio", 1.0) // Default 100% sampling
v.SetDefault("artifact.maxUploadSizeBytes", 16777216) // Default 16MB (16 * 1024 * 1024 bytes)
v.SetDefault("encryption.enabled", false)
v.SetDefault("encryption.masterKey", "")
}

func Load() (*Config, error) {
Expand Down
Loading
Loading