@geminixiang/mama 0.2.0-beta.0 → 0.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -27
- package/dist/adapter.d.ts +9 -5
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +9 -6
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +16 -13
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +10 -2
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +196 -32
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +24 -17
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +2 -0
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +109 -29
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +8 -43
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/telegram/html.d.ts +3 -0
- package/dist/adapters/telegram/html.d.ts.map +1 -0
- package/dist/adapters/telegram/html.js +98 -0
- package/dist/adapters/telegram/html.js.map +1 -0
- package/dist/agent.d.ts +4 -9
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +141 -92
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts +44 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +74 -0
- package/dist/bindings.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +53 -12
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +7 -7
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +9 -9
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +14 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +45 -10
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +20 -0
- package/dist/execution-resolver.d.ts.map +1 -0
- package/dist/execution-resolver.js +49 -0
- package/dist/execution-resolver.js.map +1 -0
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +2 -1
- package/dist/instrument.js.map +1 -1
- package/dist/link-server.d.ts +17 -0
- package/dist/link-server.d.ts.map +1 -0
- package/dist/link-server.js +899 -0
- package/dist/link-server.js.map +1 -0
- package/dist/link-token.d.ts +32 -0
- package/dist/link-token.d.ts.map +1 -0
- package/dist/link-token.js +68 -0
- package/dist/link-token.js.map +1 -0
- package/dist/log.d.ts +2 -2
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +7 -7
- package/dist/log.js.map +1 -1
- package/dist/login.d.ts +29 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +164 -0
- package/dist/login.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +226 -55
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +52 -0
- package/dist/provisioner.d.ts.map +1 -0
- package/dist/provisioner.js +291 -0
- package/dist/provisioner.js.map +1 -0
- package/dist/sandbox/container.d.ts +15 -0
- package/dist/sandbox/container.d.ts.map +1 -0
- package/dist/sandbox/container.js +122 -0
- package/dist/sandbox/container.js.map +1 -0
- package/dist/sandbox/errors.d.ts +6 -0
- package/dist/sandbox/errors.d.ts.map +1 -0
- package/dist/sandbox/errors.js +11 -0
- package/dist/sandbox/errors.js.map +1 -0
- package/dist/sandbox/firecracker.d.ts +16 -0
- package/dist/sandbox/firecracker.d.ts.map +1 -0
- package/dist/sandbox/firecracker.js +206 -0
- package/dist/sandbox/firecracker.js.map +1 -0
- package/dist/sandbox/host.d.ts +10 -0
- package/dist/sandbox/host.d.ts.map +1 -0
- package/dist/sandbox/host.js +85 -0
- package/dist/sandbox/host.js.map +1 -0
- package/dist/sandbox/image.d.ts +5 -0
- package/dist/sandbox/image.d.ts.map +1 -0
- package/dist/sandbox/image.js +30 -0
- package/dist/sandbox/image.js.map +1 -0
- package/dist/sandbox/index.d.ts +20 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +51 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/types.d.ts +51 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +2 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/sandbox/utils.d.ts +4 -0
- package/dist/sandbox/utils.d.ts.map +1 -0
- package/dist/sandbox/utils.js +51 -0
- package/dist/sandbox/utils.js.map +1 -0
- package/dist/sandbox.d.ts +1 -39
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +1 -286
- package/dist/sandbox.js.map +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +4 -2
- package/dist/sentry.js.map +1 -1
- package/dist/session-store.d.ts +2 -6
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +3 -10
- package/dist/session-store.js.map +1 -1
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +8 -8
- package/dist/store.js.map +1 -1
- package/dist/tools/event.d.ts +22 -0
- package/dist/tools/event.d.ts.map +1 -0
- package/dist/tools/event.js +104 -0
- package/dist/tools/event.js.map +1 -0
- package/dist/tools/index.d.ts +7 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/ui-copy.d.ts +12 -0
- package/dist/ui-copy.d.ts.map +1 -0
- package/dist/ui-copy.js +36 -0
- package/dist/ui-copy.js.map +1 -0
- package/dist/vault-routing.d.ts +9 -0
- package/dist/vault-routing.d.ts.map +1 -0
- package/dist/vault-routing.js +52 -0
- package/dist/vault-routing.js.map +1 -0
- package/dist/vault.d.ts +106 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +389 -0
- package/dist/vault.js.map +1 -0
- package/package.json +12 -11
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
const eventSchema = Type.Object({
|
|
5
|
+
label: Type.String({
|
|
6
|
+
description: "Brief description of the event you're scheduling (shown to user)",
|
|
7
|
+
}),
|
|
8
|
+
type: Type.Union([Type.Literal("immediate"), Type.Literal("one-shot"), Type.Literal("periodic")]),
|
|
9
|
+
text: Type.String({ description: "The reminder or event text to send when it fires" }),
|
|
10
|
+
at: Type.Optional(Type.String({
|
|
11
|
+
description: "ISO 8601 timestamp with offset, required for one-shot events",
|
|
12
|
+
})),
|
|
13
|
+
schedule: Type.Optional(Type.String({
|
|
14
|
+
description: "Cron schedule, required for periodic events",
|
|
15
|
+
})),
|
|
16
|
+
timezone: Type.Optional(Type.String({
|
|
17
|
+
description: "IANA timezone, required for periodic events",
|
|
18
|
+
})),
|
|
19
|
+
filenamePrefix: Type.Optional(Type.String({
|
|
20
|
+
description: "Optional filename prefix for the event file",
|
|
21
|
+
})),
|
|
22
|
+
});
|
|
23
|
+
export function createEventTool(workspaceDir) {
|
|
24
|
+
let eventContext = null;
|
|
25
|
+
const tool = {
|
|
26
|
+
name: "event",
|
|
27
|
+
label: "event",
|
|
28
|
+
description: "Schedule an immediate, one-shot, or periodic event for the current conversation. This automatically writes to the correct events directory and fills the current platform, conversation, conversation kind, and requester userId.",
|
|
29
|
+
parameters: eventSchema,
|
|
30
|
+
execute: async (_toolCallId, params, signal) => {
|
|
31
|
+
if (signal?.aborted) {
|
|
32
|
+
throw new Error("Operation aborted");
|
|
33
|
+
}
|
|
34
|
+
if (!eventContext) {
|
|
35
|
+
throw new Error("Event context not configured");
|
|
36
|
+
}
|
|
37
|
+
const payload = buildEventPayload(params, eventContext);
|
|
38
|
+
const eventsDir = join(workspaceDir, "events");
|
|
39
|
+
await mkdir(eventsDir, { recursive: true });
|
|
40
|
+
const prefix = sanitizeFileSegment(params.filenamePrefix || payload.type || "event");
|
|
41
|
+
const filename = `${prefix}-${Date.now()}.json`;
|
|
42
|
+
const filePath = join(eventsDir, filename);
|
|
43
|
+
await writeFile(filePath, JSON.stringify(payload) + "\n", "utf-8");
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: payload.type === "periodic"
|
|
49
|
+
? `Scheduled periodic event ${filename} for ${payload.platform}/${payload.conversationId} (${payload.schedule} ${payload.timezone})`
|
|
50
|
+
: payload.type === "one-shot"
|
|
51
|
+
? `Scheduled one-shot event ${filename} for ${payload.platform}/${payload.conversationId} at ${payload.at}`
|
|
52
|
+
: `Queued immediate event ${filename} for ${payload.platform}/${payload.conversationId}`,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
details: undefined,
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
tool,
|
|
61
|
+
setEventContext: (context) => {
|
|
62
|
+
eventContext = context;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function buildEventPayload(params, context) {
|
|
67
|
+
const base = {
|
|
68
|
+
platform: context.platform,
|
|
69
|
+
conversationId: context.conversationId,
|
|
70
|
+
conversationKind: context.conversationKind,
|
|
71
|
+
userId: context.userId,
|
|
72
|
+
text: params.text,
|
|
73
|
+
};
|
|
74
|
+
if (params.type === "immediate") {
|
|
75
|
+
return { ...base, type: "immediate" };
|
|
76
|
+
}
|
|
77
|
+
if (params.type === "one-shot") {
|
|
78
|
+
if (!params.at) {
|
|
79
|
+
throw new Error("`at` is required for one-shot events");
|
|
80
|
+
}
|
|
81
|
+
return { ...base, type: "one-shot", at: params.at };
|
|
82
|
+
}
|
|
83
|
+
if (!params.schedule) {
|
|
84
|
+
throw new Error("`schedule` is required for periodic events");
|
|
85
|
+
}
|
|
86
|
+
if (!params.timezone) {
|
|
87
|
+
throw new Error("`timezone` is required for periodic events");
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
...base,
|
|
91
|
+
type: "periodic",
|
|
92
|
+
schedule: params.schedule,
|
|
93
|
+
timezone: params.timezone,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function sanitizeFileSegment(value) {
|
|
97
|
+
const sanitized = value
|
|
98
|
+
.trim()
|
|
99
|
+
.toLowerCase()
|
|
100
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
101
|
+
.replace(/^-+|-+$/g, "");
|
|
102
|
+
return sanitized || "event";
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=event.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event.js","sourceRoot":"","sources":["../../src/tools/event.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { Type } from \"@sinclair/typebox\";\n\nconst eventSchema = Type.Object({\n label: Type.String({\n description: \"Brief description of the event you're scheduling (shown to user)\",\n }),\n type: Type.Union([Type.Literal(\"immediate\"), Type.Literal(\"one-shot\"), Type.Literal(\"periodic\")]),\n text: Type.String({ description: \"The reminder or event text to send when it fires\" }),\n at: Type.Optional(\n Type.String({\n description: \"ISO 8601 timestamp with offset, required for one-shot events\",\n }),\n ),\n schedule: Type.Optional(\n Type.String({\n description: \"Cron schedule, required for periodic events\",\n }),\n ),\n timezone: Type.Optional(\n Type.String({\n description: \"IANA timezone, required for periodic events\",\n }),\n ),\n filenamePrefix: Type.Optional(\n Type.String({\n description: \"Optional filename prefix for the event file\",\n }),\n ),\n});\n\ninterface EventToolContext {\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n}\n\ntype EventToolParams = {\n label: string;\n type: \"immediate\" | \"one-shot\" | \"periodic\";\n text: string;\n at?: string;\n schedule?: string;\n timezone?: string;\n filenamePrefix?: string;\n};\n\ntype EventPayload =\n | {\n type: \"immediate\";\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n text: string;\n }\n | {\n type: \"one-shot\";\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n text: string;\n at: string;\n }\n | {\n type: \"periodic\";\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n text: string;\n schedule: string;\n timezone: string;\n };\n\nexport function createEventTool(workspaceDir: string): {\n tool: AgentTool<typeof eventSchema>;\n setEventContext: (context: EventToolContext) => void;\n} {\n let eventContext: EventToolContext | null = null;\n\n const tool: AgentTool<typeof eventSchema> = {\n name: \"event\",\n label: \"event\",\n description:\n \"Schedule an immediate, one-shot, or periodic event for the current conversation. This automatically writes to the correct events directory and fills the current platform, conversation, conversation kind, and requester userId.\",\n parameters: eventSchema,\n execute: async (_toolCallId: string, params: EventToolParams, signal?: AbortSignal) => {\n if (signal?.aborted) {\n throw new Error(\"Operation aborted\");\n }\n\n if (!eventContext) {\n throw new Error(\"Event context not configured\");\n }\n\n const payload = buildEventPayload(params, eventContext);\n const eventsDir = join(workspaceDir, \"events\");\n await mkdir(eventsDir, { recursive: true });\n\n const prefix = sanitizeFileSegment(params.filenamePrefix || payload.type || \"event\");\n const filename = `${prefix}-${Date.now()}.json`;\n const filePath = join(eventsDir, filename);\n await writeFile(filePath, JSON.stringify(payload) + \"\\n\", \"utf-8\");\n\n return {\n content: [\n {\n type: \"text\",\n text:\n payload.type === \"periodic\"\n ? `Scheduled periodic event ${filename} for ${payload.platform}/${payload.conversationId} (${payload.schedule} ${payload.timezone})`\n : payload.type === \"one-shot\"\n ? `Scheduled one-shot event ${filename} for ${payload.platform}/${payload.conversationId} at ${payload.at}`\n : `Queued immediate event ${filename} for ${payload.platform}/${payload.conversationId}`,\n },\n ],\n details: undefined,\n };\n },\n };\n\n return {\n tool,\n setEventContext: (context: EventToolContext) => {\n eventContext = context;\n },\n };\n}\n\nfunction buildEventPayload(params: EventToolParams, context: EventToolContext): EventPayload {\n const base = {\n platform: context.platform,\n conversationId: context.conversationId,\n conversationKind: context.conversationKind,\n userId: context.userId,\n text: params.text,\n };\n\n if (params.type === \"immediate\") {\n return { ...base, type: \"immediate\" };\n }\n\n if (params.type === \"one-shot\") {\n if (!params.at) {\n throw new Error(\"`at` is required for one-shot events\");\n }\n return { ...base, type: \"one-shot\", at: params.at };\n }\n\n if (!params.schedule) {\n throw new Error(\"`schedule` is required for periodic events\");\n }\n if (!params.timezone) {\n throw new Error(\"`timezone` is required for periodic events\");\n }\n return {\n ...base,\n type: \"periodic\",\n schedule: params.schedule,\n timezone: params.timezone,\n };\n}\n\nfunction sanitizeFileSegment(value: string): string {\n const sanitized = value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n return sanitized || \"event\";\n}\n"]}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
|
2
2
|
import type { Executor } from "../sandbox.js";
|
|
3
|
-
export declare function createMamaTools(executor: Executor): {
|
|
3
|
+
export declare function createMamaTools(executor: Executor, workspaceDir: string): {
|
|
4
4
|
tools: AgentTool<any>[];
|
|
5
5
|
setUploadFunction: (fn: (filePath: string, title?: string) => Promise<void>) => void;
|
|
6
|
+
setEventContext: (context: {
|
|
7
|
+
platform: string;
|
|
8
|
+
conversationId: string;
|
|
9
|
+
conversationKind: "direct" | "shared";
|
|
10
|
+
userId: string;
|
|
11
|
+
}) => void;
|
|
6
12
|
};
|
|
7
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAE7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAO9C,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,MAAM,GACnB;IACD,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;IACxB,iBAAiB,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IACrF,eAAe,EAAE,CAAC,OAAO,EAAE;QACzB,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,gBAAgB,EAAE,QAAQ,GAAG,QAAQ,CAAC;QACtC,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,IAAI,CAAC;CACZ,CAeA","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { createAttachTool } from \"../adapters/slack/tools/attach.js\";\nimport type { Executor } from \"../sandbox.js\";\nimport { createBashTool } from \"./bash.js\";\nimport { createEditTool } from \"./edit.js\";\nimport { createEventTool } from \"./event.js\";\nimport { createReadTool } from \"./read.js\";\nimport { createWriteTool } from \"./write.js\";\n\nexport function createMamaTools(\n executor: Executor,\n workspaceDir: string,\n): {\n tools: AgentTool<any>[];\n setUploadFunction: (fn: (filePath: string, title?: string) => Promise<void>) => void;\n setEventContext: (context: {\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n }) => void;\n} {\n const { tool: attachTool, setUploadFunction } = createAttachTool();\n const { tool: eventTool, setEventContext } = createEventTool(workspaceDir);\n return {\n tools: [\n createReadTool(executor),\n createBashTool(executor),\n createEditTool(executor),\n createWriteTool(executor),\n eventTool,\n attachTool,\n ],\n setUploadFunction,\n setEventContext,\n };\n}\n"]}
|
package/dist/tools/index.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { createAttachTool } from "../adapters/slack/tools/attach.js";
|
|
2
2
|
import { createBashTool } from "./bash.js";
|
|
3
3
|
import { createEditTool } from "./edit.js";
|
|
4
|
+
import { createEventTool } from "./event.js";
|
|
4
5
|
import { createReadTool } from "./read.js";
|
|
5
6
|
import { createWriteTool } from "./write.js";
|
|
6
|
-
export function createMamaTools(executor) {
|
|
7
|
+
export function createMamaTools(executor, workspaceDir) {
|
|
7
8
|
const { tool: attachTool, setUploadFunction } = createAttachTool();
|
|
9
|
+
const { tool: eventTool, setEventContext } = createEventTool(workspaceDir);
|
|
8
10
|
return {
|
|
9
11
|
tools: [
|
|
10
12
|
createReadTool(executor),
|
|
11
13
|
createBashTool(executor),
|
|
12
14
|
createEditTool(executor),
|
|
13
15
|
createWriteTool(executor),
|
|
16
|
+
eventTool,
|
|
14
17
|
attachTool,
|
|
15
18
|
],
|
|
16
19
|
setUploadFunction,
|
|
20
|
+
setEventContext,
|
|
17
21
|
};
|
|
18
22
|
}
|
|
19
23
|
//# sourceMappingURL=index.js.map
|
package/dist/tools/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAErE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,UAAU,eAAe,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAErE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,UAAU,eAAe,CAC7B,QAAkB,EAClB,YAAoB;IAWpB,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,iBAAiB,EAAE,GAAG,gBAAgB,EAAE,CAAC;IACnE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,eAAe,EAAE,GAAG,eAAe,CAAC,YAAY,CAAC,CAAC;IAC3E,OAAO;QACL,KAAK,EAAE;YACL,cAAc,CAAC,QAAQ,CAAC;YACxB,cAAc,CAAC,QAAQ,CAAC;YACxB,cAAc,CAAC,QAAQ,CAAC;YACxB,eAAe,CAAC,QAAQ,CAAC;YACzB,SAAS;YACT,UAAU;SACX;QACD,iBAAiB;QACjB,eAAe;KAChB,CAAC;AACJ,CAAC","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-agent-core\";\nimport { createAttachTool } from \"../adapters/slack/tools/attach.js\";\nimport type { Executor } from \"../sandbox.js\";\nimport { createBashTool } from \"./bash.js\";\nimport { createEditTool } from \"./edit.js\";\nimport { createEventTool } from \"./event.js\";\nimport { createReadTool } from \"./read.js\";\nimport { createWriteTool } from \"./write.js\";\n\nexport function createMamaTools(\n executor: Executor,\n workspaceDir: string,\n): {\n tools: AgentTool<any>[];\n setUploadFunction: (fn: (filePath: string, title?: string) => Promise<void>) => void;\n setEventContext: (context: {\n platform: string;\n conversationId: string;\n conversationKind: \"direct\" | \"shared\";\n userId: string;\n }) => void;\n} {\n const { tool: attachTool, setUploadFunction } = createAttachTool();\n const { tool: eventTool, setEventContext } = createEventTool(workspaceDir);\n return {\n tools: [\n createReadTool(executor),\n createBashTool(executor),\n createEditTool(executor),\n createWriteTool(executor),\n eventTool,\n attachTool,\n ],\n setUploadFunction,\n setEventContext,\n };\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Bot, PlatformInfo } from "./adapter.js";
|
|
2
|
+
export declare const PRODUCT_NAME = "mama";
|
|
3
|
+
type PlatformSource = Bot | PlatformInfo | string;
|
|
4
|
+
export declare function formatNothingRunning(source: PlatformSource): string;
|
|
5
|
+
export declare function formatStopping(source: PlatformSource): string;
|
|
6
|
+
export declare function formatStopped(source: PlatformSource): string;
|
|
7
|
+
export declare function formatAlreadyWorking(source: PlatformSource, stopCommand: string, options?: {
|
|
8
|
+
scope?: "thread";
|
|
9
|
+
}): string;
|
|
10
|
+
export declare function formatForceStopped(source: PlatformSource, actorLabel: string): string;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=ui-copy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui-copy.d.ts","sourceRoot":"","sources":["../src/ui-copy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEtD,eAAO,MAAM,YAAY,SAAS,CAAC;AAEnC,KAAK,cAAc,GAAG,GAAG,GAAG,YAAY,GAAG,MAAM,CAAC;AAoBlD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAEnE;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAE7D;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAE5D;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,cAAc,EACtB,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,QAAQ,CAAA;CAAE,GAC7B,MAAM,CAMR;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAErF","sourcesContent":["import type { Bot, PlatformInfo } from \"./adapter.js\";\n\nexport const PRODUCT_NAME = \"mama\";\n\ntype PlatformSource = Bot | PlatformInfo | string;\n\nfunction resolvePlatformName(source: PlatformSource): string {\n if (typeof source === \"string\") return source;\n if (\"getPlatformInfo\" in source) return source.getPlatformInfo().name;\n return source.name;\n}\n\nfunction supportsHtmlFormatting(platformName: string): boolean {\n return platformName === \"telegram\";\n}\n\nfunction formatItalic(platformName: string, text: string): string {\n return supportsHtmlFormatting(platformName) ? text : `_${text}_`;\n}\n\nfunction formatCode(platformName: string, text: string): string {\n return supportsHtmlFormatting(platformName) ? `<code>${text}</code>` : `\\`${text}\\``;\n}\n\nexport function formatNothingRunning(source: PlatformSource): string {\n return formatItalic(resolvePlatformName(source), \"Nothing running.\");\n}\n\nexport function formatStopping(source: PlatformSource): string {\n return formatItalic(resolvePlatformName(source), \"Stopping…\");\n}\n\nexport function formatStopped(source: PlatformSource): string {\n return formatItalic(resolvePlatformName(source), \"Stopped.\");\n}\n\nexport function formatAlreadyWorking(\n source: PlatformSource,\n stopCommand: string,\n options?: { scope?: \"thread\" },\n): string {\n const platformName = resolvePlatformName(source);\n const command = formatCode(platformName, stopCommand);\n const prefix =\n options?.scope === \"thread\" ? \"Already working in this thread.\" : \"Already working.\";\n return formatItalic(platformName, `${prefix} Send ${command} to cancel.`);\n}\n\nexport function formatForceStopped(source: PlatformSource, actorLabel: string): string {\n return formatItalic(resolvePlatformName(source), `Force stopped by ${actorLabel}.`);\n}\n"]}
|
package/dist/ui-copy.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const PRODUCT_NAME = "mama";
|
|
2
|
+
function resolvePlatformName(source) {
|
|
3
|
+
if (typeof source === "string")
|
|
4
|
+
return source;
|
|
5
|
+
if ("getPlatformInfo" in source)
|
|
6
|
+
return source.getPlatformInfo().name;
|
|
7
|
+
return source.name;
|
|
8
|
+
}
|
|
9
|
+
function supportsHtmlFormatting(platformName) {
|
|
10
|
+
return platformName === "telegram";
|
|
11
|
+
}
|
|
12
|
+
function formatItalic(platformName, text) {
|
|
13
|
+
return supportsHtmlFormatting(platformName) ? text : `_${text}_`;
|
|
14
|
+
}
|
|
15
|
+
function formatCode(platformName, text) {
|
|
16
|
+
return supportsHtmlFormatting(platformName) ? `<code>${text}</code>` : `\`${text}\``;
|
|
17
|
+
}
|
|
18
|
+
export function formatNothingRunning(source) {
|
|
19
|
+
return formatItalic(resolvePlatformName(source), "Nothing running.");
|
|
20
|
+
}
|
|
21
|
+
export function formatStopping(source) {
|
|
22
|
+
return formatItalic(resolvePlatformName(source), "Stopping…");
|
|
23
|
+
}
|
|
24
|
+
export function formatStopped(source) {
|
|
25
|
+
return formatItalic(resolvePlatformName(source), "Stopped.");
|
|
26
|
+
}
|
|
27
|
+
export function formatAlreadyWorking(source, stopCommand, options) {
|
|
28
|
+
const platformName = resolvePlatformName(source);
|
|
29
|
+
const command = formatCode(platformName, stopCommand);
|
|
30
|
+
const prefix = options?.scope === "thread" ? "Already working in this thread." : "Already working.";
|
|
31
|
+
return formatItalic(platformName, `${prefix} Send ${command} to cancel.`);
|
|
32
|
+
}
|
|
33
|
+
export function formatForceStopped(source, actorLabel) {
|
|
34
|
+
return formatItalic(resolvePlatformName(source), `Force stopped by ${actorLabel}.`);
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=ui-copy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui-copy.js","sourceRoot":"","sources":["../src/ui-copy.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AAInC,SAAS,mBAAmB,CAAC,MAAsB;IACjD,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IAC9C,IAAI,iBAAiB,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC,eAAe,EAAE,CAAC,IAAI,CAAC;IACtE,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC;AAED,SAAS,sBAAsB,CAAC,YAAoB;IAClD,OAAO,YAAY,KAAK,UAAU,CAAC;AACrC,CAAC;AAED,SAAS,YAAY,CAAC,YAAoB,EAAE,IAAY;IACtD,OAAO,sBAAsB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC;AACnE,CAAC;AAED,SAAS,UAAU,CAAC,YAAoB,EAAE,IAAY;IACpD,OAAO,sBAAsB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC;AACvF,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MAAsB;IACzD,OAAO,YAAY,CAAC,mBAAmB,CAAC,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAsB;IACnD,OAAO,YAAY,CAAC,mBAAmB,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,MAAsB;IAClD,OAAO,YAAY,CAAC,mBAAmB,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,MAAsB,EACtB,WAAmB,EACnB,OAA8B;IAE9B,MAAM,YAAY,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAC;IACjD,MAAM,OAAO,GAAG,UAAU,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IACtD,MAAM,MAAM,GACV,OAAO,EAAE,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,iCAAiC,CAAC,CAAC,CAAC,kBAAkB,CAAC;IACvF,OAAO,YAAY,CAAC,YAAY,EAAE,GAAG,MAAM,SAAS,OAAO,aAAa,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAAsB,EAAE,UAAkB;IAC3E,OAAO,YAAY,CAAC,mBAAmB,CAAC,MAAM,CAAC,EAAE,oBAAoB,UAAU,GAAG,CAAC,CAAC;AACtF,CAAC","sourcesContent":["import type { Bot, PlatformInfo } from \"./adapter.js\";\n\nexport const PRODUCT_NAME = \"mama\";\n\ntype PlatformSource = Bot | PlatformInfo | string;\n\nfunction resolvePlatformName(source: PlatformSource): string {\n if (typeof source === \"string\") return source;\n if (\"getPlatformInfo\" in source) return source.getPlatformInfo().name;\n return source.name;\n}\n\nfunction supportsHtmlFormatting(platformName: string): boolean {\n return platformName === \"telegram\";\n}\n\nfunction formatItalic(platformName: string, text: string): string {\n return supportsHtmlFormatting(platformName) ? text : `_${text}_`;\n}\n\nfunction formatCode(platformName: string, text: string): string {\n return supportsHtmlFormatting(platformName) ? `<code>${text}</code>` : `\\`${text}\\``;\n}\n\nexport function formatNothingRunning(source: PlatformSource): string {\n return formatItalic(resolvePlatformName(source), \"Nothing running.\");\n}\n\nexport function formatStopping(source: PlatformSource): string {\n return formatItalic(resolvePlatformName(source), \"Stopping…\");\n}\n\nexport function formatStopped(source: PlatformSource): string {\n return formatItalic(resolvePlatformName(source), \"Stopped.\");\n}\n\nexport function formatAlreadyWorking(\n source: PlatformSource,\n stopCommand: string,\n options?: { scope?: \"thread\" },\n): string {\n const platformName = resolvePlatformName(source);\n const command = formatCode(platformName, stopCommand);\n const prefix =\n options?.scope === \"thread\" ? \"Already working in this thread.\" : \"Already working.\";\n return formatItalic(platformName, `${prefix} Send ${command} to cancel.`);\n}\n\nexport function formatForceStopped(source: PlatformSource, actorLabel: string): string {\n return formatItalic(resolvePlatformName(source), `Force stopped by ${actorLabel}.`);\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UserBindingStore } from "./bindings.js";
|
|
2
|
+
import type { SandboxConfig } from "./sandbox.js";
|
|
3
|
+
import type { VaultEntry, VaultManager } from "./vault.js";
|
|
4
|
+
export declare function resolveActorVaultKey(baseConfig: SandboxConfig, vaultManager: Pick<VaultManager, "hasEntry">, bindingStore: Pick<UserBindingStore, "resolve"> | undefined, platform: string, userId: string): string;
|
|
5
|
+
export declare function createManagedVaultEntry(platform: string, userId: string, vaultKey: string, withImageSandbox?: boolean): VaultEntry;
|
|
6
|
+
export declare function containerSharedVaultId(containerName: string): string;
|
|
7
|
+
export declare function createSharedContainerVaultEntry(containerName: string): VaultEntry;
|
|
8
|
+
export declare function ensureSandboxVaultEntry(baseConfig: SandboxConfig, vaultManager: Pick<VaultManager, "addEntry" | "ensureImageSandboxEntry">, platform: string, userId: string, vaultKey: string): void;
|
|
9
|
+
//# sourceMappingURL=vault-routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault-routing.d.ts","sourceRoot":"","sources":["../src/vault-routing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE3D,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,aAAa,EACzB,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,EAC5C,YAAY,EAAE,IAAI,CAAC,gBAAgB,EAAE,SAAS,CAAC,GAAG,SAAS,EAC3D,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,MAAM,CAeR;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,gBAAgB,UAAQ,GACvB,UAAU,CAaZ;AAED,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,wBAAgB,+BAA+B,CAAC,aAAa,EAAE,MAAM,GAAG,UAAU,CAIjF;AAED,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,aAAa,EACzB,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,UAAU,GAAG,yBAAyB,CAAC,EACxE,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,IAAI,CAYN","sourcesContent":["import type { UserBindingStore } from \"./bindings.js\";\nimport { DockerContainerManager } from \"./provisioner.js\";\nimport type { SandboxConfig } from \"./sandbox.js\";\nimport type { VaultEntry, VaultManager } from \"./vault.js\";\n\nexport function resolveActorVaultKey(\n baseConfig: SandboxConfig,\n vaultManager: Pick<VaultManager, \"hasEntry\">,\n bindingStore: Pick<UserBindingStore, \"resolve\"> | undefined,\n platform: string,\n userId: string,\n): string {\n if (baseConfig.type === \"container\") {\n return containerSharedVaultId(baseConfig.container);\n }\n\n const binding = bindingStore?.resolve(platform, userId);\n if (binding) {\n return binding.vaultId;\n }\n\n if (vaultManager.hasEntry(userId)) {\n return userId;\n }\n\n return baseConfig.type === \"image\" ? DockerContainerManager.vaultId(platform, userId) : userId;\n}\n\nexport function createManagedVaultEntry(\n platform: string,\n userId: string,\n vaultKey: string,\n withImageSandbox = false,\n): VaultEntry {\n return {\n displayName: `${platform}:${userId}`,\n platform: asVaultPlatform(platform),\n ...(withImageSandbox\n ? {\n sandbox: {\n type: \"image\" as const,\n container: DockerContainerManager.containerName(vaultKey),\n },\n }\n : {}),\n };\n}\n\nexport function containerSharedVaultId(containerName: string): string {\n return `container-${containerName}`;\n}\n\nexport function createSharedContainerVaultEntry(containerName: string): VaultEntry {\n return {\n displayName: `container:${containerName}`,\n };\n}\n\nexport function ensureSandboxVaultEntry(\n baseConfig: SandboxConfig,\n vaultManager: Pick<VaultManager, \"addEntry\" | \"ensureImageSandboxEntry\">,\n platform: string,\n userId: string,\n vaultKey: string,\n): void {\n if (baseConfig.type === \"image\") {\n vaultManager.ensureImageSandboxEntry(\n vaultKey,\n createManagedVaultEntry(platform, userId, vaultKey, true),\n );\n return;\n }\n\n if (baseConfig.type === \"container\") {\n vaultManager.addEntry(vaultKey, createSharedContainerVaultEntry(baseConfig.container));\n }\n}\n\nfunction asVaultPlatform(platform: string): VaultEntry[\"platform\"] | undefined {\n if (platform === \"slack\" || platform === \"discord\" || platform === \"telegram\") {\n return platform;\n }\n return undefined;\n}\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { DockerContainerManager } from "./provisioner.js";
|
|
2
|
+
export function resolveActorVaultKey(baseConfig, vaultManager, bindingStore, platform, userId) {
|
|
3
|
+
if (baseConfig.type === "container") {
|
|
4
|
+
return containerSharedVaultId(baseConfig.container);
|
|
5
|
+
}
|
|
6
|
+
const binding = bindingStore?.resolve(platform, userId);
|
|
7
|
+
if (binding) {
|
|
8
|
+
return binding.vaultId;
|
|
9
|
+
}
|
|
10
|
+
if (vaultManager.hasEntry(userId)) {
|
|
11
|
+
return userId;
|
|
12
|
+
}
|
|
13
|
+
return baseConfig.type === "image" ? DockerContainerManager.vaultId(platform, userId) : userId;
|
|
14
|
+
}
|
|
15
|
+
export function createManagedVaultEntry(platform, userId, vaultKey, withImageSandbox = false) {
|
|
16
|
+
return {
|
|
17
|
+
displayName: `${platform}:${userId}`,
|
|
18
|
+
platform: asVaultPlatform(platform),
|
|
19
|
+
...(withImageSandbox
|
|
20
|
+
? {
|
|
21
|
+
sandbox: {
|
|
22
|
+
type: "image",
|
|
23
|
+
container: DockerContainerManager.containerName(vaultKey),
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
: {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function containerSharedVaultId(containerName) {
|
|
30
|
+
return `container-${containerName}`;
|
|
31
|
+
}
|
|
32
|
+
export function createSharedContainerVaultEntry(containerName) {
|
|
33
|
+
return {
|
|
34
|
+
displayName: `container:${containerName}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function ensureSandboxVaultEntry(baseConfig, vaultManager, platform, userId, vaultKey) {
|
|
38
|
+
if (baseConfig.type === "image") {
|
|
39
|
+
vaultManager.ensureImageSandboxEntry(vaultKey, createManagedVaultEntry(platform, userId, vaultKey, true));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (baseConfig.type === "container") {
|
|
43
|
+
vaultManager.addEntry(vaultKey, createSharedContainerVaultEntry(baseConfig.container));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function asVaultPlatform(platform) {
|
|
47
|
+
if (platform === "slack" || platform === "discord" || platform === "telegram") {
|
|
48
|
+
return platform;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=vault-routing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault-routing.js","sourceRoot":"","sources":["../src/vault-routing.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAI1D,MAAM,UAAU,oBAAoB,CAClC,UAAyB,EACzB,YAA4C,EAC5C,YAA2D,EAC3D,QAAgB,EAChB,MAAc;IAEd,IAAI,UAAU,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACpC,OAAO,sBAAsB,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IACtD,CAAC;IAED,MAAM,OAAO,GAAG,YAAY,EAAE,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACxD,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,OAAO,CAAC,OAAO,CAAC;IACzB,CAAC;IAED,IAAI,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAClC,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO,UAAU,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;AACjG,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,QAAgB,EAChB,MAAc,EACd,QAAgB,EAChB,gBAAgB,GAAG,KAAK;IAExB,OAAO;QACL,WAAW,EAAE,GAAG,QAAQ,IAAI,MAAM,EAAE;QACpC,QAAQ,EAAE,eAAe,CAAC,QAAQ,CAAC;QACnC,GAAG,CAAC,gBAAgB;YAClB,CAAC,CAAC;gBACE,OAAO,EAAE;oBACP,IAAI,EAAE,OAAgB;oBACtB,SAAS,EAAE,sBAAsB,CAAC,aAAa,CAAC,QAAQ,CAAC;iBAC1D;aACF;YACH,CAAC,CAAC,EAAE,CAAC;KACR,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,aAAqB;IAC1D,OAAO,aAAa,aAAa,EAAE,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,+BAA+B,CAAC,aAAqB;IACnE,OAAO;QACL,WAAW,EAAE,aAAa,aAAa,EAAE;KAC1C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,UAAyB,EACzB,YAAwE,EACxE,QAAgB,EAChB,MAAc,EACd,QAAgB;IAEhB,IAAI,UAAU,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAChC,YAAY,CAAC,uBAAuB,CAClC,QAAQ,EACR,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAC1D,CAAC;QACF,OAAO;IACT,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACpC,YAAY,CAAC,QAAQ,CAAC,QAAQ,EAAE,+BAA+B,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IACzF,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,QAAgB;IACvC,IAAI,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,UAAU,EAAE,CAAC;QAC9E,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC","sourcesContent":["import type { UserBindingStore } from \"./bindings.js\";\nimport { DockerContainerManager } from \"./provisioner.js\";\nimport type { SandboxConfig } from \"./sandbox.js\";\nimport type { VaultEntry, VaultManager } from \"./vault.js\";\n\nexport function resolveActorVaultKey(\n baseConfig: SandboxConfig,\n vaultManager: Pick<VaultManager, \"hasEntry\">,\n bindingStore: Pick<UserBindingStore, \"resolve\"> | undefined,\n platform: string,\n userId: string,\n): string {\n if (baseConfig.type === \"container\") {\n return containerSharedVaultId(baseConfig.container);\n }\n\n const binding = bindingStore?.resolve(platform, userId);\n if (binding) {\n return binding.vaultId;\n }\n\n if (vaultManager.hasEntry(userId)) {\n return userId;\n }\n\n return baseConfig.type === \"image\" ? DockerContainerManager.vaultId(platform, userId) : userId;\n}\n\nexport function createManagedVaultEntry(\n platform: string,\n userId: string,\n vaultKey: string,\n withImageSandbox = false,\n): VaultEntry {\n return {\n displayName: `${platform}:${userId}`,\n platform: asVaultPlatform(platform),\n ...(withImageSandbox\n ? {\n sandbox: {\n type: \"image\" as const,\n container: DockerContainerManager.containerName(vaultKey),\n },\n }\n : {}),\n };\n}\n\nexport function containerSharedVaultId(containerName: string): string {\n return `container-${containerName}`;\n}\n\nexport function createSharedContainerVaultEntry(containerName: string): VaultEntry {\n return {\n displayName: `container:${containerName}`,\n };\n}\n\nexport function ensureSandboxVaultEntry(\n baseConfig: SandboxConfig,\n vaultManager: Pick<VaultManager, \"addEntry\" | \"ensureImageSandboxEntry\">,\n platform: string,\n userId: string,\n vaultKey: string,\n): void {\n if (baseConfig.type === \"image\") {\n vaultManager.ensureImageSandboxEntry(\n vaultKey,\n createManagedVaultEntry(platform, userId, vaultKey, true),\n );\n return;\n }\n\n if (baseConfig.type === \"container\") {\n vaultManager.addEntry(vaultKey, createSharedContainerVaultEntry(baseConfig.container));\n }\n}\n\nfunction asVaultPlatform(platform: string): VaultEntry[\"platform\"] | undefined {\n if (platform === \"slack\" || platform === \"discord\" || platform === \"telegram\") {\n return platform;\n }\n return undefined;\n}\n"]}
|
package/dist/vault.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { SandboxConfig } from "./sandbox.js";
|
|
2
|
+
/** Shape of workspace/vaults/vault.json */
|
|
3
|
+
export interface VaultConfig {
|
|
4
|
+
vaults: Record<string, VaultEntry>;
|
|
5
|
+
}
|
|
6
|
+
/** Per-user vault mount entry in vault.json */
|
|
7
|
+
export interface VaultMountEntry {
|
|
8
|
+
source: string;
|
|
9
|
+
target?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Per-user vault entry in vault.json */
|
|
12
|
+
export interface VaultEntry {
|
|
13
|
+
displayName: string;
|
|
14
|
+
platform?: "slack" | "discord" | "telegram";
|
|
15
|
+
/** Subdirs/files in vault dir to mount into sandbox (e.g. [".gcloud", ".ssh", ".kube"]) */
|
|
16
|
+
mounts?: Array<string | VaultMountEntry>;
|
|
17
|
+
/** Whether to load env file as environment variables (default: true if env file exists) */
|
|
18
|
+
envFile?: boolean;
|
|
19
|
+
/** Per-user sandbox config override */
|
|
20
|
+
sandbox?: {
|
|
21
|
+
type?: "image" | "firecracker" | "host" | "container" | "docker";
|
|
22
|
+
container?: string;
|
|
23
|
+
image?: string;
|
|
24
|
+
vmId?: string;
|
|
25
|
+
sshUser?: string;
|
|
26
|
+
sshPort?: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface ResolvedVaultMount {
|
|
30
|
+
source: string;
|
|
31
|
+
target: string;
|
|
32
|
+
}
|
|
33
|
+
/** Resolved vault ready for use at runtime */
|
|
34
|
+
export interface ResolvedVault {
|
|
35
|
+
userId: string;
|
|
36
|
+
displayName: string;
|
|
37
|
+
/** Absolute path to vault directory */
|
|
38
|
+
dir: string;
|
|
39
|
+
/** Absolute mount specs */
|
|
40
|
+
mounts: ResolvedVaultMount[];
|
|
41
|
+
/** Parsed from env file */
|
|
42
|
+
env: Record<string, string>;
|
|
43
|
+
sandboxOverride?: VaultEntry["sandbox"];
|
|
44
|
+
}
|
|
45
|
+
export interface VaultManager {
|
|
46
|
+
/** Return true when vault.json contains this exact key. */
|
|
47
|
+
hasEntry(key: string): boolean;
|
|
48
|
+
/** Resolve vault for a user; returns undefined when no entry exists. */
|
|
49
|
+
resolve(userId: string): ResolvedVault | undefined;
|
|
50
|
+
/** Get sandbox config with credential injection for a user */
|
|
51
|
+
getSandboxConfig(userId: string, baseConfig: SandboxConfig): SandboxConfig;
|
|
52
|
+
/** List all configured vaults */
|
|
53
|
+
list(): ResolvedVault[];
|
|
54
|
+
/** Re-read vault.json without restart */
|
|
55
|
+
reload(): void;
|
|
56
|
+
/** Check if vault system is enabled (vault.json exists) */
|
|
57
|
+
isEnabled(): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Add a vault entry and persist to disk.
|
|
60
|
+
* No-op if the key already exists (idempotent).
|
|
61
|
+
*/
|
|
62
|
+
addEntry(key: string, entry: VaultEntry): void;
|
|
63
|
+
/**
|
|
64
|
+
* Ensure a vault entry has image sandbox metadata.
|
|
65
|
+
* Creates the entry when missing and upgrades existing entries that lack sandbox.type.
|
|
66
|
+
*/
|
|
67
|
+
ensureImageSandboxEntry(key: string, entry: VaultEntry): void;
|
|
68
|
+
/** Merge environment variables into vaults/<key>/env and persist them to disk. */
|
|
69
|
+
upsertEnv(key: string, env: Record<string, string>): void;
|
|
70
|
+
/** Write a private file into vaults/<key>/ and ensure it is mounted into the sandbox. */
|
|
71
|
+
upsertFile(key: string, relativePath: string, content: string, targetPath?: string): void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse a KEY=VALUE env file. Supports:
|
|
75
|
+
* - Lines starting with # are comments
|
|
76
|
+
* - Empty lines are skipped
|
|
77
|
+
* - Values can be quoted with single or double quotes (quotes are stripped)
|
|
78
|
+
* - No variable expansion
|
|
79
|
+
* - The value is everything after the first `=` to end of line (no inline comments)
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseEnvFile(content: string): Record<string, string>;
|
|
82
|
+
export declare class FileVaultManager implements VaultManager {
|
|
83
|
+
private config;
|
|
84
|
+
private readonly vaultsDir;
|
|
85
|
+
private readonly configPath;
|
|
86
|
+
constructor(stateDir: string);
|
|
87
|
+
reload(): void;
|
|
88
|
+
/** Warn for legacy or insecure vault sandbox overrides that are no longer allowed. */
|
|
89
|
+
private warnUnsupportedSandboxTypes;
|
|
90
|
+
isEnabled(): boolean;
|
|
91
|
+
hasEntry(key: string): boolean;
|
|
92
|
+
resolve(userId: string): ResolvedVault | undefined;
|
|
93
|
+
getSandboxConfig(userId: string, baseConfig: SandboxConfig): SandboxConfig;
|
|
94
|
+
list(): ResolvedVault[];
|
|
95
|
+
addEntry(key: string, entry: VaultEntry): void;
|
|
96
|
+
ensureImageSandboxEntry(key: string, entry: VaultEntry): void;
|
|
97
|
+
upsertEnv(key: string, env: Record<string, string>): void;
|
|
98
|
+
upsertFile(key: string, relativePath: string, content: string, targetPath?: string): void;
|
|
99
|
+
private persistConfig;
|
|
100
|
+
private readConfigFromDisk;
|
|
101
|
+
private ensureMountEntry;
|
|
102
|
+
private buildResolved;
|
|
103
|
+
private resolveMountEntry;
|
|
104
|
+
}
|
|
105
|
+
export declare function defaultVaultTargetPath(relativePath: string): string;
|
|
106
|
+
//# sourceMappingURL=vault.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault.d.ts","sourceRoot":"","sources":["../src/vault.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n chmodSync,\n closeSync,\n constants as fsConstants,\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeSync,\n} from \"fs\";\nimport { randomBytes } from \"crypto\";\nimport { basename, dirname, isAbsolute, join, normalize, sep } from \"path\";\nimport type { SandboxConfig } from \"./sandbox.js\";\n\nconst PRIVATE_DIR_MODE = 0o700;\nconst PRIVATE_FILE_MODE = 0o600;\n\n// ── Types ──────────────────────────────────────────────────────────────────────\n\n/** Shape of workspace/vaults/vault.json */\nexport interface VaultConfig {\n vaults: Record<string, VaultEntry>;\n}\n\n/** Per-user vault mount entry in vault.json */\nexport interface VaultMountEntry {\n source: string;\n target?: string;\n}\n\n/** Per-user vault entry in vault.json */\nexport interface VaultEntry {\n displayName: string;\n platform?: \"slack\" | \"discord\" | \"telegram\";\n /** Subdirs/files in vault dir to mount into sandbox (e.g. [\".gcloud\", \".ssh\", \".kube\"]) */\n mounts?: Array<string | VaultMountEntry>;\n /** Whether to load env file as environment variables (default: true if env file exists) */\n envFile?: boolean;\n /** Per-user sandbox config override */\n sandbox?: {\n type?: \"image\" | \"firecracker\" | \"host\" | \"container\" | \"docker\";\n container?: string;\n image?: string;\n vmId?: string;\n sshUser?: string;\n sshPort?: number;\n };\n}\n\nexport interface ResolvedVaultMount {\n source: string;\n target: string;\n}\n\n/** Resolved vault ready for use at runtime */\nexport interface ResolvedVault {\n userId: string;\n displayName: string;\n /** Absolute path to vault directory */\n dir: string;\n /** Absolute mount specs */\n mounts: ResolvedVaultMount[];\n /** Parsed from env file */\n env: Record<string, string>;\n sandboxOverride?: VaultEntry[\"sandbox\"];\n}\n\nexport interface VaultManager {\n /** Return true when vault.json contains this exact key. */\n hasEntry(key: string): boolean;\n /** Resolve vault for a user; returns undefined when no entry exists. */\n resolve(userId: string): ResolvedVault | undefined;\n /** Get sandbox config with credential injection for a user */\n getSandboxConfig(userId: string, baseConfig: SandboxConfig): SandboxConfig;\n /** List all configured vaults */\n list(): ResolvedVault[];\n /** Re-read vault.json without restart */\n reload(): void;\n /** Check if vault system is enabled (vault.json exists) */\n isEnabled(): boolean;\n /**\n * Add a vault entry and persist to disk.\n * No-op if the key already exists (idempotent).\n */\n addEntry(key: string, entry: VaultEntry): void;\n /**\n * Ensure a vault entry has image sandbox metadata.\n * Creates the entry when missing and upgrades existing entries that lack sandbox.type.\n */\n ensureImageSandboxEntry(key: string, entry: VaultEntry): void;\n /** Merge environment variables into vaults/<key>/env and persist them to disk. */\n upsertEnv(key: string, env: Record<string, string>): void;\n /** Write a private file into vaults/<key>/ and ensure it is mounted into the sandbox. */\n upsertFile(key: string, relativePath: string, content: string, targetPath?: string): void;\n}\n\n// ── parseEnvFile ───────────────────────────────────────────────────────────────\n\n/**\n * Parse a KEY=VALUE env file. Supports:\n * - Lines starting with # are comments\n * - Empty lines are skipped\n * - Values can be quoted with single or double quotes (quotes are stripped)\n * - No variable expansion\n * - The value is everything after the first `=` to end of line (no inline comments)\n */\nexport function parseEnvFile(content: string): Record<string, string> {\n const env: Record<string, string> = {};\n const lines = content.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\").split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n const eqIndex = trimmed.indexOf(\"=\");\n if (eqIndex === -1) continue;\n\n const key = trimmed.slice(0, eqIndex).trim();\n if (!key) continue;\n\n let value = trimmed.slice(eqIndex + 1);\n\n // Strip matching quotes\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n\n env[key] = value;\n }\n\n return env;\n}\n\n// ── FileVaultManager ───────────────────────────────────────────────────────────\n\nexport class FileVaultManager implements VaultManager {\n private config: VaultConfig | null = null;\n private readonly vaultsDir: string;\n private readonly configPath: string;\n\n constructor(stateDir: string) {\n this.vaultsDir = join(stateDir, \"vaults\");\n this.configPath = join(this.vaultsDir, \"vault.json\");\n this.reload();\n }\n\n reload(): void {\n if (!existsSync(this.configPath)) {\n this.config = null;\n return;\n }\n\n try {\n const raw = readFileSync(this.configPath, \"utf-8\");\n const parsed = JSON.parse(raw);\n\n if (\n !parsed ||\n typeof parsed !== \"object\" ||\n !parsed.vaults ||\n typeof parsed.vaults !== \"object\"\n ) {\n console.error(`vault: malformed vault.json — expected { vaults: { ... } }`);\n this.config = null;\n return;\n }\n\n this.config = parsed as VaultConfig;\n this.warnUnsupportedSandboxTypes();\n } catch (err) {\n console.error(`vault: failed to read ${this.configPath}:`, err);\n this.config = null;\n }\n }\n\n /** Warn for legacy or insecure vault sandbox overrides that are no longer allowed. */\n private warnUnsupportedSandboxTypes(): void {\n if (!this.config) return;\n for (const [key, entry] of Object.entries(this.config.vaults)) {\n if (entry.sandbox?.type === \"host\") {\n console.error(\n `vault: \"${key}\" uses sandbox.type=host, which is blocked for credential isolation. ` +\n \"Use sandbox.type=image or sandbox.type=firecracker.\",\n );\n }\n if (entry.sandbox?.type === \"container\" || entry.sandbox?.type === \"docker\") {\n console.error(\n `vault: \"${key}\" uses sandbox.type=${entry.sandbox.type}, which is blocked for credential isolation. ` +\n \"Use sandbox.type=image for per-user containers or sandbox.type=firecracker.\",\n );\n }\n }\n }\n\n isEnabled(): boolean {\n return this.config !== null;\n }\n\n hasEntry(key: string): boolean {\n return !!this.config?.vaults[key];\n }\n\n resolve(userId: string): ResolvedVault | undefined {\n const entry = this.config?.vaults[userId];\n if (!entry) return undefined;\n return this.buildResolved(userId, entry);\n }\n\n getSandboxConfig(userId: string, baseConfig: SandboxConfig): SandboxConfig {\n const vault = this.resolve(userId);\n if (!vault?.sandboxOverride) return baseConfig;\n\n const override = vault.sandboxOverride;\n\n if (override.type === \"image\") {\n if (baseConfig.type !== \"image\") {\n throw new Error(\n `vault \"${userId}\" sets sandbox.type=image, but base sandbox is \"${baseConfig.type}\". ` +\n \"Use --sandbox=image:<image> to enable per-user managed containers.\",\n );\n }\n const container = override.container || `mama-sandbox-${userId}`;\n return { type: \"container\", container };\n }\n\n if (override.type === \"firecracker\") {\n if (!override.vmId) return baseConfig;\n if (baseConfig.type !== \"firecracker\") {\n throw new Error(\n `vault \"${userId}\" sets sandbox.type=firecracker, but base sandbox is \"${baseConfig.type}\". ` +\n \"Use --sandbox=firecracker:<vm-id>:<host-path> so /workspace stays mapped to the real workspace.\",\n );\n }\n return {\n type: \"firecracker\",\n vmId: override.vmId,\n hostPath: baseConfig.hostPath,\n sshUser: override.sshUser,\n sshPort: override.sshPort,\n };\n }\n\n if (override.type === \"host\") {\n throw new Error(\n `vault \"${userId}\" uses sandbox.type=host, which is blocked for credential isolation. ` +\n \"Use sandbox.type=image or sandbox.type=firecracker.\",\n );\n }\n\n if (override.type === \"container\" || override.type === \"docker\") {\n throw new Error(\n `vault \"${userId}\" uses sandbox.type=${override.type}, which is blocked for credential isolation. ` +\n \"Use sandbox.type=image for per-user containers or sandbox.type=firecracker.\",\n );\n }\n\n // No type override — return base config unchanged\n return baseConfig;\n }\n\n list(): ResolvedVault[] {\n if (!this.config) return [];\n\n const results: ResolvedVault[] = [];\n for (const [key, entry] of Object.entries(this.config.vaults)) {\n results.push(this.buildResolved(key, entry));\n }\n return results;\n }\n\n addEntry(key: string, entry: VaultEntry): void {\n if (!this.config) {\n this.config = { vaults: {} };\n }\n // Idempotent: skip if already exists\n if (this.config.vaults[key]) return;\n this.config.vaults[key] = entry;\n this.persistConfig();\n }\n\n ensureImageSandboxEntry(key: string, entry: VaultEntry): void {\n if (entry.sandbox?.type !== \"image\") {\n throw new Error(`vault: ensureImageSandboxEntry requires sandbox.type=image for \"${key}\"`);\n }\n\n if (!this.config) {\n this.config = { vaults: {} };\n }\n\n const existing = this.config.vaults[key];\n if (!existing) {\n this.config.vaults[key] = entry;\n this.persistConfig();\n return;\n }\n\n let nextEntry = existing;\n let changed = false;\n\n if (!existing.platform && entry.platform) {\n nextEntry = { ...nextEntry, platform: entry.platform };\n changed = true;\n }\n\n const existingSandbox = existing.sandbox;\n if (!existingSandbox?.type) {\n nextEntry = { ...nextEntry, sandbox: entry.sandbox };\n changed = true;\n } else if (\n existingSandbox.type === \"image\" &&\n !existingSandbox.container &&\n entry.sandbox.container\n ) {\n nextEntry = {\n ...nextEntry,\n sandbox: { ...existingSandbox, container: entry.sandbox.container },\n };\n changed = true;\n }\n\n if (!changed) {\n return;\n }\n\n this.config.vaults[key] = nextEntry;\n this.persistConfig();\n }\n\n upsertEnv(key: string, env: Record<string, string>): void {\n const dir = join(this.vaultsDir, key);\n const envPath = join(dir, \"env\");\n ensurePrivateDir(this.vaultsDir);\n ensurePrivateDir(dir);\n const existing = existsSync(envPath)\n ? parseEnvFile(readFileSync(envPath, \"utf-8\"))\n : ({} as Record<string, string>);\n const merged = { ...existing, ...env };\n const content =\n Object.entries(merged)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([envKey, value]) => `${envKey}=${value}`)\n .join(\"\\n\") + \"\\n\";\n atomicWritePrivateFile(envPath, content);\n }\n\n upsertFile(key: string, relativePath: string, content: string, targetPath?: string): void {\n const normalizedPath = normalizeVaultRelativePath(relativePath);\n const normalizedTarget = normalizeVaultTargetPath(targetPath);\n if (!normalizedPath || (targetPath !== undefined && !normalizedTarget)) {\n throw new Error(`vault: invalid relative secret file path for \"${key}\": ${relativePath}`);\n }\n\n const dir = join(this.vaultsDir, key);\n const filePath = join(dir, normalizedPath);\n\n ensurePrivateDir(this.vaultsDir);\n ensurePrivateDir(dir);\n const parentDir = dirname(filePath);\n if (parentDir !== dir) ensurePrivateDir(parentDir);\n atomicWritePrivateFile(filePath, content);\n this.ensureMountEntry(key, normalizedPath, normalizedTarget);\n }\n\n // ── private ────────────────────────────────────────────────────────────────\n\n private persistConfig(): void {\n ensurePrivateDir(this.vaultsDir);\n\n // Preserve concurrent external edits: pull in any entries that appear on\n // disk but not in our in-memory view, so a background edit (e.g. another\n // admin adding a user) is not silently dropped by the next upsert here.\n // Individual field edits still follow last-writer-wins per key.\n const onDisk = this.readConfigFromDisk();\n if (onDisk && this.config) {\n for (const [key, entry] of Object.entries(onDisk.vaults)) {\n if (!(key in this.config.vaults)) {\n this.config.vaults[key] = entry;\n }\n }\n }\n\n atomicWritePrivateFile(this.configPath, JSON.stringify(this.config, null, 2) + \"\\n\");\n }\n\n private readConfigFromDisk(): VaultConfig | null {\n if (!existsSync(this.configPath)) return null;\n try {\n const parsed = JSON.parse(readFileSync(this.configPath, \"utf-8\"));\n if (\n !parsed ||\n typeof parsed !== \"object\" ||\n !parsed.vaults ||\n typeof parsed.vaults !== \"object\"\n ) {\n return null;\n }\n return parsed as VaultConfig;\n } catch {\n return null;\n }\n }\n\n private ensureMountEntry(key: string, relativePath: string, targetPath?: string): void {\n if (!this.config?.vaults[key]) {\n throw new Error(`vault: cannot add mount \"${relativePath}\" for missing entry \"${key}\"`);\n }\n\n const existing = this.config.vaults[key];\n const mounts = existing.mounts ?? [];\n if (\n mounts.some((mount) =>\n typeof mount === \"string\"\n ? mount === relativePath && !targetPath\n : mount.source === relativePath && mount.target === targetPath,\n )\n ) {\n return;\n }\n\n this.config.vaults[key] = {\n ...existing,\n mounts: [...mounts, targetPath ? { source: relativePath, target: targetPath } : relativePath],\n };\n this.persistConfig();\n }\n\n private buildResolved(key: string, entry: VaultEntry): ResolvedVault {\n const dir = join(this.vaultsDir, key);\n\n const mounts = (entry.mounts ?? [])\n .map((mount) => this.resolveMountEntry(dir, mount))\n .filter((mount): mount is ResolvedVaultMount => mount !== undefined);\n\n let env: Record<string, string> = {};\n const envPath = join(dir, \"env\");\n if (entry.envFile !== false && existsSync(envPath)) {\n try {\n env = parseEnvFile(readFileSync(envPath, \"utf-8\"));\n } catch (err) {\n console.error(`vault: failed to parse env file for \"${key}\":`, err);\n }\n }\n\n return {\n userId: key,\n displayName: entry.displayName,\n dir,\n mounts,\n env,\n sandboxOverride: entry.sandbox,\n };\n }\n\n private resolveMountEntry(\n dir: string,\n mount: string | VaultMountEntry,\n ): ResolvedVaultMount | undefined {\n if (typeof mount === \"string\") {\n const normalizedSource = normalizeVaultRelativePath(mount);\n if (!normalizedSource) return undefined;\n return {\n source: join(dir, normalizedSource),\n target: defaultVaultTargetPath(normalizedSource),\n };\n }\n\n if (!mount || typeof mount !== \"object\") return undefined;\n const normalizedSource = normalizeVaultRelativePath(mount.source);\n if (!normalizedSource) return undefined;\n const normalizedTarget = normalizeVaultTargetPath(mount.target);\n return {\n source: join(dir, normalizedSource),\n target: normalizedTarget ?? defaultVaultTargetPath(normalizedSource),\n };\n }\n}\n\nfunction ensurePrivateDir(path: string): void {\n mkdirSync(path, { recursive: true, mode: PRIVATE_DIR_MODE });\n chmodSync(path, PRIVATE_DIR_MODE);\n}\n\n/**\n * Write `content` to `targetPath` with mode 0600, even when `targetPath`\n * already exists. Uses O_CREAT|O_EXCL on a temp sibling (so the kernel\n * guarantees permissions at creation, not after a racy chmod) and then\n * rename(2) into place for atomicity. Readers never see a torn write.\n */\nfunction atomicWritePrivateFile(targetPath: string, content: string): void {\n const dir = dirname(targetPath);\n const tmpPath = join(\n dir,\n `.${basename(targetPath)}.${process.pid}.${randomBytes(8).toString(\"hex\")}.tmp`,\n );\n const fd = openSync(\n tmpPath,\n fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL,\n PRIVATE_FILE_MODE,\n );\n try {\n writeSync(fd, content);\n } catch (err) {\n try {\n unlinkSync(tmpPath);\n } catch {\n // ignore — original error is more informative\n }\n throw err;\n } finally {\n closeSync(fd);\n }\n try {\n renameSync(tmpPath, targetPath);\n } catch (err) {\n try {\n unlinkSync(tmpPath);\n } catch {\n // ignore\n }\n throw err;\n }\n}\n\nfunction normalizeVaultRelativePath(relativePath: string): string | undefined {\n const trimmed = relativePath.trim();\n if (!trimmed || isAbsolute(trimmed)) return undefined;\n\n const normalized = normalize(trimmed).split(sep).join(\"/\");\n if (!normalized || normalized === \".\" || normalized === \"..\" || normalized.startsWith(\"../\")) {\n return undefined;\n }\n return normalized;\n}\n\nfunction normalizeVaultTargetPath(targetPath?: string): string | undefined {\n if (targetPath === undefined) {\n return undefined;\n }\n\n const trimmed = targetPath.trim();\n if (!trimmed || !trimmed.startsWith(\"/\")) {\n return undefined;\n }\n\n const normalized = normalize(trimmed).split(sep).join(\"/\");\n return normalized.startsWith(\"/\") ? normalized : undefined;\n}\n\nexport function defaultVaultTargetPath(relativePath: string): string {\n const normalized = normalizeVaultRelativePath(relativePath) ?? relativePath.replace(/^\\/+/, \"\");\n return `/root/${normalized}`;\n}\n"]}
|