open-managed-agents 0.1.0
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/LICENSE +93 -0
- package/README.md +248 -0
- package/bin/oma.mjs +499 -0
- package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
- package/dist/scripts/alpha-smoke-models.mjs +92 -0
- package/dist/scripts/alpha-smoke.mjs +581 -0
- package/dist/scripts/oma-doctor.js +230 -0
- package/dist/scripts/oma-models.js +299 -0
- package/dist/scripts/oma-workspaces.js +133 -0
- package/dist/src/control-plane/admin/auth.js +43 -0
- package/dist/src/control-plane/admin/routes.js +76 -0
- package/dist/src/control-plane/admin/service.js +102 -0
- package/dist/src/control-plane/admission.js +90 -0
- package/dist/src/control-plane/agents/routes.js +63 -0
- package/dist/src/control-plane/agents/service.js +687 -0
- package/dist/src/control-plane/agents/store.js +391 -0
- package/dist/src/control-plane/agents/types.js +1 -0
- package/dist/src/control-plane/api-constants.js +3 -0
- package/dist/src/control-plane/app.js +907 -0
- package/dist/src/control-plane/console/auth.js +68 -0
- package/dist/src/control-plane/console/static.js +112 -0
- package/dist/src/control-plane/deployment-runtime-config.js +328 -0
- package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
- package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
- package/dist/src/control-plane/deployment-storage.js +354 -0
- package/dist/src/control-plane/egress/image.js +48 -0
- package/dist/src/control-plane/egress/policy.js +571 -0
- package/dist/src/control-plane/egress/presets.js +77 -0
- package/dist/src/control-plane/egress/proxy.js +72 -0
- package/dist/src/control-plane/egress/ssrf.js +106 -0
- package/dist/src/control-plane/egress/utils/debug.js +14 -0
- package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
- package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
- package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
- package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
- package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
- package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
- package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
- package/dist/src/control-plane/environments/routes.js +54 -0
- package/dist/src/control-plane/environments/service.js +112 -0
- package/dist/src/control-plane/environments/store.js +132 -0
- package/dist/src/control-plane/environments/types.js +1 -0
- package/dist/src/control-plane/errors.js +81 -0
- package/dist/src/control-plane/events/broadcaster.js +217 -0
- package/dist/src/control-plane/events/constants.js +2 -0
- package/dist/src/control-plane/events/persist.js +39 -0
- package/dist/src/control-plane/events/request.js +190 -0
- package/dist/src/control-plane/events/routes.js +159 -0
- package/dist/src/control-plane/events/runtime-helpers.js +134 -0
- package/dist/src/control-plane/events/service.js +1985 -0
- package/dist/src/control-plane/events/session-guards.js +29 -0
- package/dist/src/control-plane/events/sse.js +6 -0
- package/dist/src/control-plane/events/store.js +772 -0
- package/dist/src/control-plane/events/tool-persistence.js +102 -0
- package/dist/src/control-plane/events/types.js +21 -0
- package/dist/src/control-plane/files/routes.js +121 -0
- package/dist/src/control-plane/files/service.js +65 -0
- package/dist/src/control-plane/files/store-common.js +87 -0
- package/dist/src/control-plane/files/store-local.js +488 -0
- package/dist/src/control-plane/files/store-memory.js +270 -0
- package/dist/src/control-plane/files/store.js +2 -0
- package/dist/src/control-plane/files/types.js +7 -0
- package/dist/src/control-plane/http.js +25 -0
- package/dist/src/control-plane/ids.js +43 -0
- package/dist/src/control-plane/logging.js +263 -0
- package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
- package/dist/src/control-plane/models/catalog.js +78 -0
- package/dist/src/control-plane/models/config-security.js +376 -0
- package/dist/src/control-plane/models/deployment-config.js +93 -0
- package/dist/src/control-plane/models/routes.js +32 -0
- package/dist/src/control-plane/models/service.js +138 -0
- package/dist/src/control-plane/observability/instruments.js +65 -0
- package/dist/src/control-plane/observability/metrics.js +125 -0
- package/dist/src/control-plane/observability/routes.js +95 -0
- package/dist/src/control-plane/openapi/document.js +282 -0
- package/dist/src/control-plane/openapi/routes.js +78 -0
- package/dist/src/control-plane/request-idempotency.js +85 -0
- package/dist/src/control-plane/secrets/envelope.js +132 -0
- package/dist/src/control-plane/secrets/master-key.js +44 -0
- package/dist/src/control-plane/secrets/routes.js +20 -0
- package/dist/src/control-plane/secrets/service.js +71 -0
- package/dist/src/control-plane/secrets/store.js +212 -0
- package/dist/src/control-plane/secrets/types.js +3 -0
- package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
- package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
- package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
- package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
- package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
- package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
- package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
- package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
- package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
- package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
- package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
- package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
- package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
- package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
- package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
- package/dist/src/control-plane/sessions/pi/translator.js +100 -0
- package/dist/src/control-plane/sessions/request.js +164 -0
- package/dist/src/control-plane/sessions/resources.js +63 -0
- package/dist/src/control-plane/sessions/routes.js +88 -0
- package/dist/src/control-plane/sessions/serialize.js +24 -0
- package/dist/src/control-plane/sessions/service.js +733 -0
- package/dist/src/control-plane/sessions/store.js +538 -0
- package/dist/src/control-plane/sessions/types.js +1 -0
- package/dist/src/control-plane/skills/archive.js +169 -0
- package/dist/src/control-plane/skills/routes.js +41 -0
- package/dist/src/control-plane/skills/service.js +29 -0
- package/dist/src/control-plane/skills/store.js +233 -0
- package/dist/src/control-plane/skills/types.js +5 -0
- package/dist/src/control-plane/sqlite-transaction.js +15 -0
- package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
- package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
- package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
- package/dist/src/control-plane/vaults/routes.js +66 -0
- package/dist/src/control-plane/vaults/service.js +553 -0
- package/dist/src/control-plane/vaults/store.js +691 -0
- package/dist/src/control-plane/vaults/types.js +1 -0
- package/dist/src/control-plane/wake-loop.js +65 -0
- package/dist/src/control-plane/wiring.js +92 -0
- package/dist/src/control-plane/workspace.js +4 -0
- package/dist/src/control-plane/workspaces/store.js +158 -0
- package/dist/src/egress-proxy-main.js +103 -0
- package/dist/src/main.js +124 -0
- package/dist/src/types/agents.js +1 -0
- package/dist/src/types/common.js +1 -0
- package/dist/src/types/environments.js +1 -0
- package/dist/src/types/events.js +121 -0
- package/dist/src/types/files.js +1 -0
- package/dist/src/types/json.js +19 -0
- package/dist/src/types/sessions.js +1 -0
- package/dist/ui/managed-agents-console/README.md +52 -0
- package/dist/ui/managed-agents-console/docs/agents.md +23 -0
- package/dist/ui/managed-agents-console/docs/console.md +23 -0
- package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
- package/dist/ui/managed-agents-console/docs/environments.md +27 -0
- package/dist/ui/managed-agents-console/docs/events.md +19 -0
- package/dist/ui/managed-agents-console/docs/files.md +17 -0
- package/dist/ui/managed-agents-console/docs/github.md +11 -0
- package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
- package/dist/ui/managed-agents-console/docs/memory.md +11 -0
- package/dist/ui/managed-agents-console/docs/migration.md +17 -0
- package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
- package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
- package/dist/ui/managed-agents-console/docs/overview.md +41 -0
- package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
- package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
- package/dist/ui/managed-agents-console/docs/reference.md +27 -0
- package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
- package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
- package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
- package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
- package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
- package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
- package/dist/ui/managed-agents-console/docs/skills.md +15 -0
- package/dist/ui/managed-agents-console/docs/tools.md +23 -0
- package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
- package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
- package/dist/ui/managed-agents-console/index.html +47 -0
- package/dist/ui/managed-agents-console/serve.mjs +116 -0
- package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
- package/dist/ui/managed-agents-console/src/api.js +1043 -0
- package/dist/ui/managed-agents-console/src/app.jsx +633 -0
- package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
- package/dist/ui/managed-agents-console/src/console.css +733 -0
- package/dist/ui/managed-agents-console/src/data.js +188 -0
- package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
- package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
- package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
- package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
- package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
- package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
- package/dist/ui/managed-agents-console/src/sse.js +185 -0
- package/dist/ui/managed-agents-console/src/states.jsx +90 -0
- package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
- package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
- package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
- package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
- package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
- package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
- package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
- package/dist/ui/openapi-docs/VENDOR.md +27 -0
- package/dist/ui/openapi-docs/index.html +16 -0
- package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
- package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
- package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
- package/package.json +53 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export function createWakeLoop(opts) {
|
|
2
|
+
const now = opts.now ?? Date.now;
|
|
3
|
+
let timer;
|
|
4
|
+
let running;
|
|
5
|
+
let closed = false;
|
|
6
|
+
const schedule = (delayMs) => {
|
|
7
|
+
if (closed)
|
|
8
|
+
return;
|
|
9
|
+
if (timer !== undefined)
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
timer = setTimeout(() => {
|
|
12
|
+
timer = undefined;
|
|
13
|
+
void cycle();
|
|
14
|
+
}, delayMs);
|
|
15
|
+
};
|
|
16
|
+
const cycle = async () => {
|
|
17
|
+
if (closed || running !== undefined)
|
|
18
|
+
return;
|
|
19
|
+
const work = (async () => {
|
|
20
|
+
try {
|
|
21
|
+
const dueAt = await opts.nextWakeAt();
|
|
22
|
+
if (closed)
|
|
23
|
+
return;
|
|
24
|
+
if (dueAt !== null && dueAt.getTime() <= now()) {
|
|
25
|
+
await opts.run();
|
|
26
|
+
if (closed)
|
|
27
|
+
return;
|
|
28
|
+
schedule(opts.minSleepMs);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const untilDue = dueAt === null ? opts.maxSleepMs : dueAt.getTime() - now();
|
|
32
|
+
schedule(Math.min(opts.maxSleepMs, Math.max(opts.minSleepMs, untilDue)));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
opts.onError(error);
|
|
36
|
+
schedule(opts.minSleepMs);
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
running = work;
|
|
40
|
+
try {
|
|
41
|
+
await work;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
running = undefined;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
schedule(0);
|
|
48
|
+
return {
|
|
49
|
+
wake() {
|
|
50
|
+
if (closed || running !== undefined)
|
|
51
|
+
return;
|
|
52
|
+
schedule(0);
|
|
53
|
+
},
|
|
54
|
+
async close() {
|
|
55
|
+
if (closed)
|
|
56
|
+
return;
|
|
57
|
+
closed = true;
|
|
58
|
+
if (timer !== undefined) {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
timer = undefined;
|
|
61
|
+
}
|
|
62
|
+
await running;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { resolveSessionEgressBundle } from "./egress/policy.js";
|
|
3
|
+
import { DEFAULT_SIDECAR_PORT } from "./sessions/pi/sandbox/docker-egress.js";
|
|
4
|
+
/**
|
|
5
|
+
* Per-session egress bundle resolution (plan 0117e-3, Option A): session ->
|
|
6
|
+
* environment -> networking config -> secrets, resolved at sandbox-create
|
|
7
|
+
* time inside the docker factory closure. Returns undefined only when the
|
|
8
|
+
* environment has no networking or a valid hosted empty allowlist. Every
|
|
9
|
+
* other present networking shape is parsed and fails closed. Mints a fresh
|
|
10
|
+
* URL-safe proxy-auth token per session.
|
|
11
|
+
*/
|
|
12
|
+
export function createSessionEgressBundleResolver(stores) {
|
|
13
|
+
return async (workspaceId, sessionId, context) => {
|
|
14
|
+
// Normal prompt-time sandbox creation reads the persisted session row. The
|
|
15
|
+
// file-resource create path prepares its sandbox before that row is
|
|
16
|
+
// committed, so DefaultSessionService passes the already-validated
|
|
17
|
+
// environmentId as a creation-time hint.
|
|
18
|
+
const environmentId = context?.environmentId;
|
|
19
|
+
const session = environmentId === undefined
|
|
20
|
+
? stores.sessions.retrieveAny(workspaceId, sessionId)
|
|
21
|
+
: undefined;
|
|
22
|
+
if (environmentId === undefined && !session)
|
|
23
|
+
return undefined;
|
|
24
|
+
const resolvedEnvironmentId = environmentId ?? session.environment_id;
|
|
25
|
+
const environment = stores.environments.retrieveAny(workspaceId, resolvedEnvironmentId);
|
|
26
|
+
if (!environment)
|
|
27
|
+
return undefined;
|
|
28
|
+
const resolved = resolveSessionEgressBundle({
|
|
29
|
+
environmentConfig: environment.config,
|
|
30
|
+
revealSecret: (name) => stores.secrets?.reveal(workspaceId, name),
|
|
31
|
+
listenPort: DEFAULT_SIDECAR_PORT,
|
|
32
|
+
proxyAuthToken: randomBytes(24).toString("hex"),
|
|
33
|
+
});
|
|
34
|
+
if (resolved === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
return { bundle: resolved.bundle, sandboxEnv: resolved.sandboxEnv };
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function createFileMountResolver(sessionStore, fileStorage) {
|
|
40
|
+
return async (workspaceId, sessionId) => {
|
|
41
|
+
const snapshots = sessionStore.getFileMountSnapshots(workspaceId, sessionId);
|
|
42
|
+
return Promise.all(snapshots.map(async (snapshot) => snapshotToRuntimeMount(workspaceId, fileStorage, snapshot)));
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function createSkillSnapshotsProvider(sessionStore) {
|
|
46
|
+
return (workspaceId, sessionId) => sessionStore.getSkillSnapshots(workspaceId, sessionId).map((skill) => ({
|
|
47
|
+
name: skill.name,
|
|
48
|
+
description: skill.description,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
async function snapshotToRuntimeMount(workspaceId, fileStorage, snapshot) {
|
|
52
|
+
const bytes = await fileStorage.openInternalSnapshotBytes(workspaceId, snapshot.snapshot_file_id);
|
|
53
|
+
if (!bytes) {
|
|
54
|
+
throw new Error(`Session file snapshot ${snapshot.snapshot_file_id} not found`);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
kind: snapshot.kind,
|
|
58
|
+
mountPath: snapshot.mount_path,
|
|
59
|
+
snapshotFileId: snapshot.snapshot_file_id,
|
|
60
|
+
sha256: snapshot.sha256,
|
|
61
|
+
sizeBytes: snapshot.size_bytes,
|
|
62
|
+
bytes,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function createStoreBackedAgentRevisionProvider(opts) {
|
|
66
|
+
return (workspaceId, sessionId, context) => {
|
|
67
|
+
const session = opts.sessions.retrieveAny(workspaceId, sessionId);
|
|
68
|
+
const agentId = session?.agent.id ?? context?.agentId;
|
|
69
|
+
const version = session?.agent.version ?? context?.agentVersion;
|
|
70
|
+
if (!agentId || version === undefined)
|
|
71
|
+
return undefined;
|
|
72
|
+
const agent = opts.agents.retrieveVersion(workspaceId, agentId, version);
|
|
73
|
+
if (!agent) {
|
|
74
|
+
throw new Error(`Pinned agent revision not found: ${agentId}@${version}`);
|
|
75
|
+
}
|
|
76
|
+
return { model: agent.model, system: agent.system };
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function createStoreBackedCustomToolsProvider(opts) {
|
|
80
|
+
return (workspaceId, sessionId, context) => {
|
|
81
|
+
const session = opts.sessions.retrieveAny(workspaceId, sessionId);
|
|
82
|
+
const agentId = session?.agent.id ?? context?.agentId;
|
|
83
|
+
const agentVersion = session?.agent.version ?? context?.agentVersion;
|
|
84
|
+
if (!agentId || agentVersion === undefined)
|
|
85
|
+
return [];
|
|
86
|
+
const agent = opts.agents.retrieveVersion(workspaceId, agentId, agentVersion);
|
|
87
|
+
if (!agent)
|
|
88
|
+
return [];
|
|
89
|
+
const tools = agent.tools.filter((tool) => tool.type === "custom");
|
|
90
|
+
return tools;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { DEFAULT_WORKSPACE_ID } from "../workspace.js";
|
|
4
|
+
import { uuidv7 } from "../../types/events.js";
|
|
5
|
+
// Plan 0113 D2/D3: opaque `oma_`-prefixed 256-bit keys, SHA-256 digests at
|
|
6
|
+
// rest, tombstone revocation, idempotent wrk_default seeding.
|
|
7
|
+
const SCHEMA = `
|
|
8
|
+
PRAGMA foreign_keys = ON;
|
|
9
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
10
|
+
workspace_id TEXT PRIMARY KEY,
|
|
11
|
+
name TEXT NOT NULL,
|
|
12
|
+
created_at TEXT NOT NULL
|
|
13
|
+
);
|
|
14
|
+
CREATE TABLE IF NOT EXISTS workspace_api_keys (
|
|
15
|
+
key_sha256 TEXT PRIMARY KEY,
|
|
16
|
+
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id),
|
|
17
|
+
label TEXT NOT NULL,
|
|
18
|
+
created_at TEXT NOT NULL,
|
|
19
|
+
revoked_at TEXT
|
|
20
|
+
);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS workspace_api_keys_by_workspace
|
|
22
|
+
ON workspace_api_keys (workspace_id, key_sha256);
|
|
23
|
+
CREATE TABLE IF NOT EXISTS console_sessions (
|
|
24
|
+
token_sha256 TEXT PRIMARY KEY,
|
|
25
|
+
kind TEXT NOT NULL CHECK (kind IN ('workspace_key', 'admin', 'admin_workspace')),
|
|
26
|
+
workspace_id TEXT REFERENCES workspaces(workspace_id),
|
|
27
|
+
credential_sha256 TEXT NOT NULL,
|
|
28
|
+
created_at TEXT NOT NULL,
|
|
29
|
+
expires_at TEXT NOT NULL
|
|
30
|
+
);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS console_sessions_by_expiry
|
|
32
|
+
ON console_sessions (expires_at);
|
|
33
|
+
`;
|
|
34
|
+
export const WORKSPACE_API_KEY_PREFIX = "oma_";
|
|
35
|
+
export function newWorkspaceId() {
|
|
36
|
+
return `wrk_${uuidv7()}`;
|
|
37
|
+
}
|
|
38
|
+
export function hashWorkspaceApiKey(plaintextKey) {
|
|
39
|
+
return createHash("sha256").update(plaintextKey).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
export class SqliteWorkspaceStore {
|
|
42
|
+
db;
|
|
43
|
+
insertWorkspaceStmt;
|
|
44
|
+
getWorkspaceStmt;
|
|
45
|
+
insertKeyStmt;
|
|
46
|
+
authenticateStmt;
|
|
47
|
+
revokeKeyStmt;
|
|
48
|
+
listKeysStmt;
|
|
49
|
+
listWorkspacesStmt;
|
|
50
|
+
getKeyStmt;
|
|
51
|
+
countKeysStmt;
|
|
52
|
+
insertConsoleSessionStmt;
|
|
53
|
+
getConsoleSessionStmt;
|
|
54
|
+
deleteConsoleSessionStmt;
|
|
55
|
+
constructor(db) {
|
|
56
|
+
this.db = db;
|
|
57
|
+
this.db.exec(SCHEMA);
|
|
58
|
+
this.insertWorkspaceStmt = this.db.prepare(`INSERT INTO workspaces (workspace_id, name, created_at) VALUES (?, ?, ?)`);
|
|
59
|
+
this.getWorkspaceStmt = this.db.prepare(`SELECT workspace_id, name, created_at FROM workspaces WHERE workspace_id = ?`);
|
|
60
|
+
this.insertKeyStmt = this.db.prepare(`INSERT INTO workspace_api_keys (key_sha256, workspace_id, label, created_at, revoked_at)
|
|
61
|
+
VALUES (?, ?, ?, ?, NULL)`);
|
|
62
|
+
this.authenticateStmt = this.db.prepare(`SELECT workspace_id FROM workspace_api_keys
|
|
63
|
+
WHERE key_sha256 = ? AND revoked_at IS NULL`);
|
|
64
|
+
this.revokeKeyStmt = this.db.prepare(`UPDATE workspace_api_keys
|
|
65
|
+
SET revoked_at = COALESCE(revoked_at, ?)
|
|
66
|
+
WHERE key_sha256 = ?`);
|
|
67
|
+
this.listKeysStmt = this.db.prepare(`SELECT key_sha256, workspace_id, label, created_at, revoked_at
|
|
68
|
+
FROM workspace_api_keys
|
|
69
|
+
WHERE workspace_id = ?
|
|
70
|
+
ORDER BY key_sha256 ASC`);
|
|
71
|
+
this.listWorkspacesStmt = this.db.prepare(`SELECT workspace_id, name, created_at FROM workspaces
|
|
72
|
+
ORDER BY workspace_id ASC`);
|
|
73
|
+
this.getKeyStmt = this.db.prepare(`SELECT key_sha256, workspace_id, label, created_at, revoked_at
|
|
74
|
+
FROM workspace_api_keys
|
|
75
|
+
WHERE key_sha256 = ?`);
|
|
76
|
+
this.countKeysStmt = this.db.prepare(`SELECT COUNT(*) AS n FROM workspace_api_keys`);
|
|
77
|
+
this.insertConsoleSessionStmt = this.db.prepare(`INSERT INTO console_sessions
|
|
78
|
+
(token_sha256, kind, workspace_id, credential_sha256, created_at, expires_at)
|
|
79
|
+
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
80
|
+
this.getConsoleSessionStmt = this.db.prepare(`SELECT token_sha256, kind, workspace_id, credential_sha256, created_at, expires_at
|
|
81
|
+
FROM console_sessions
|
|
82
|
+
WHERE token_sha256 = ? AND expires_at > ?`);
|
|
83
|
+
this.deleteConsoleSessionStmt = this.db.prepare(`DELETE FROM console_sessions WHERE token_sha256 = ?`);
|
|
84
|
+
this.db.prepare(`INSERT OR IGNORE INTO workspaces (workspace_id, name, created_at)
|
|
85
|
+
VALUES (?, 'Default workspace', ?)`).run(DEFAULT_WORKSPACE_ID, new Date().toISOString());
|
|
86
|
+
}
|
|
87
|
+
static open(path) {
|
|
88
|
+
return new SqliteWorkspaceStore(new DatabaseSync(path));
|
|
89
|
+
}
|
|
90
|
+
createWorkspace(name, workspaceId = newWorkspaceId()) {
|
|
91
|
+
const row = {
|
|
92
|
+
workspace_id: workspaceId,
|
|
93
|
+
name,
|
|
94
|
+
created_at: new Date().toISOString(),
|
|
95
|
+
};
|
|
96
|
+
this.insertWorkspaceStmt.run(row.workspace_id, row.name, row.created_at);
|
|
97
|
+
return row;
|
|
98
|
+
}
|
|
99
|
+
getWorkspace(workspaceId) {
|
|
100
|
+
return this.getWorkspaceStmt.get(workspaceId);
|
|
101
|
+
}
|
|
102
|
+
listWorkspaces() {
|
|
103
|
+
return this.listWorkspacesStmt.all();
|
|
104
|
+
}
|
|
105
|
+
getKey(keySha256) {
|
|
106
|
+
return this.getKeyStmt.get(keySha256);
|
|
107
|
+
}
|
|
108
|
+
mintKey(workspaceId, label) {
|
|
109
|
+
if (!this.getWorkspace(workspaceId)) {
|
|
110
|
+
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
111
|
+
}
|
|
112
|
+
const plaintextKey = `${WORKSPACE_API_KEY_PREFIX}${randomBytes(32).toString("base64url")}`;
|
|
113
|
+
const keySha256 = hashWorkspaceApiKey(plaintextKey);
|
|
114
|
+
this.insertKeyStmt.run(keySha256, workspaceId, label, new Date().toISOString());
|
|
115
|
+
return { plaintextKey, keySha256, workspaceId, label };
|
|
116
|
+
}
|
|
117
|
+
// Counts every key ever minted, revoked included: first boot means "no key
|
|
118
|
+
// was ever issued", not "no key currently works".
|
|
119
|
+
countApiKeys() {
|
|
120
|
+
return this.countKeysStmt.get().n;
|
|
121
|
+
}
|
|
122
|
+
authenticate(plaintextKey) {
|
|
123
|
+
const row = this.authenticateStmt.get(hashWorkspaceApiKey(plaintextKey));
|
|
124
|
+
return row?.workspace_id;
|
|
125
|
+
}
|
|
126
|
+
authenticateKeySha256(keySha256) {
|
|
127
|
+
const row = this.authenticateStmt.get(keySha256);
|
|
128
|
+
return row?.workspace_id;
|
|
129
|
+
}
|
|
130
|
+
mintConsoleSession(input) {
|
|
131
|
+
const plaintextToken = `ocs_${randomBytes(32).toString("base64url")}`;
|
|
132
|
+
const row = {
|
|
133
|
+
token_sha256: hashWorkspaceApiKey(plaintextToken),
|
|
134
|
+
kind: input.kind,
|
|
135
|
+
workspace_id: input.workspaceId ?? null,
|
|
136
|
+
credential_sha256: input.credentialSha256,
|
|
137
|
+
created_at: new Date().toISOString(),
|
|
138
|
+
expires_at: input.expiresAt.toISOString(),
|
|
139
|
+
};
|
|
140
|
+
this.insertConsoleSessionStmt.run(row.token_sha256, row.kind, row.workspace_id, row.credential_sha256, row.created_at, row.expires_at);
|
|
141
|
+
return { plaintextToken, row };
|
|
142
|
+
}
|
|
143
|
+
getConsoleSession(plaintextToken) {
|
|
144
|
+
return this.getConsoleSessionStmt.get(hashWorkspaceApiKey(plaintextToken), new Date().toISOString());
|
|
145
|
+
}
|
|
146
|
+
revokeConsoleSession(plaintextToken) {
|
|
147
|
+
return this.deleteConsoleSessionStmt.run(hashWorkspaceApiKey(plaintextToken)).changes > 0;
|
|
148
|
+
}
|
|
149
|
+
revokeKey(keySha256) {
|
|
150
|
+
return this.revokeKeyStmt.run(new Date().toISOString(), keySha256).changes > 0;
|
|
151
|
+
}
|
|
152
|
+
listKeys(workspaceId) {
|
|
153
|
+
return this.listKeysStmt.all(workspaceId);
|
|
154
|
+
}
|
|
155
|
+
close() {
|
|
156
|
+
this.db.close();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session egress proxy sidecar entrypoint (plan 0117d; ADR 0016 §2/§3).
|
|
3
|
+
*
|
|
4
|
+
* The vendored `createEgressProxy` runs OUT of the long-lived control-plane
|
|
5
|
+
* process, in its own short-lived container, so the control plane never joins
|
|
6
|
+
* the sandbox's network (the sandbox's sole route out is this proxy). It is
|
|
7
|
+
* launched with the SAME appliance image, only a different CMD — no second
|
|
8
|
+
* image to ship.
|
|
9
|
+
*
|
|
10
|
+
* Contract (all via env, since the sidecar takes no CLI args):
|
|
11
|
+
* OMA_EGRESS_BUNDLE_PATH read-only mount of the serialized SessionEgressBundle
|
|
12
|
+
* (policy + grants + resolved secrets + auth token).
|
|
13
|
+
* Holds real secret material — mounted ONLY here.
|
|
14
|
+
* OMA_EGRESS_SHARED_DIR a dir shared with the control plane. The sidecar
|
|
15
|
+
* writes `ca.crt` (its ephemeral MITM CA cert) and,
|
|
16
|
+
* once listening, a `ready` marker. The CA PRIVATE
|
|
17
|
+
* KEY never leaves this container.
|
|
18
|
+
* OMA_EGRESS_BIND_HOST listen address (default 0.0.0.0 — the sidecar is
|
|
19
|
+
* only reachable on the per-session --internal net).
|
|
20
|
+
*
|
|
21
|
+
* The proxy inherits the default SSRF/private-IP deny (we never set the
|
|
22
|
+
* dangerous test flag) and never sets mutateHeadersPlaintext, so secrets are
|
|
23
|
+
* injected only on the TLS-terminated leg.
|
|
24
|
+
*/
|
|
25
|
+
import { copyFileSync, readFileSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { pathToFileURL } from "node:url";
|
|
28
|
+
import { createEgressProxy, createMitmCA, disposeMitmCA, } from "./control-plane/egress/proxy.js";
|
|
29
|
+
import { buildHooksFromBundle, } from "./control-plane/egress/policy.js";
|
|
30
|
+
import { log } from "./control-plane/logging.js";
|
|
31
|
+
function requiredEnv(env, key) {
|
|
32
|
+
const value = env[key];
|
|
33
|
+
if (typeof value !== "string" || value === "") {
|
|
34
|
+
throw new Error(`egress proxy sidecar requires ${key}`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
export function loadSessionEgressBundle(path) {
|
|
39
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
40
|
+
if (typeof parsed !== "object" ||
|
|
41
|
+
parsed === null ||
|
|
42
|
+
typeof parsed.listenPort !== "number" ||
|
|
43
|
+
typeof parsed.proxyAuthToken !== "string" ||
|
|
44
|
+
!Array.isArray(parsed.grants) ||
|
|
45
|
+
typeof parsed.policy !== "object") {
|
|
46
|
+
throw new Error(`egress bundle at ${path} is malformed`);
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Start the sidecar proxy. Generates the MITM CA in-process, publishes its
|
|
52
|
+
* cert (not its key) to the shared dir, listens, then drops the `ready`
|
|
53
|
+
* marker the control plane waits on before it launches the sandbox.
|
|
54
|
+
*/
|
|
55
|
+
export async function startEgressProxySidecar(env) {
|
|
56
|
+
const bundlePath = requiredEnv(env, "OMA_EGRESS_BUNDLE_PATH");
|
|
57
|
+
const sharedDir = requiredEnv(env, "OMA_EGRESS_SHARED_DIR");
|
|
58
|
+
const bindHost = env.OMA_EGRESS_BIND_HOST ?? "0.0.0.0";
|
|
59
|
+
const bundle = loadSessionEgressBundle(bundlePath);
|
|
60
|
+
// Ephemeral CA minted here; only the trust BUNDLE is published (never the
|
|
61
|
+
// key, which could forge certs for any host — it stays in this container).
|
|
62
|
+
// The bundle is the MITM CA followed by the host's public roots: the sandbox
|
|
63
|
+
// trust env vars (SSL_CERT_FILE, CURL_CA_BUNDLE, ...) REPLACE the client's
|
|
64
|
+
// store, so publishing the CA alone would break verification for any host we
|
|
65
|
+
// do NOT terminate (opaque-tunnel / cert-pinned upstreams talk to the real
|
|
66
|
+
// upstream cert). trustBundlePath already contains CA + roots.
|
|
67
|
+
const ca = createMitmCA({});
|
|
68
|
+
copyFileSync(ca.trustBundlePath, join(sharedDir, "ca.crt"));
|
|
69
|
+
const server = createEgressProxy({
|
|
70
|
+
...buildHooksFromBundle(bundle),
|
|
71
|
+
mitmCA: ca,
|
|
72
|
+
proxyAuthToken: bundle.proxyAuthToken,
|
|
73
|
+
});
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
75
|
+
server.once("error", reject);
|
|
76
|
+
server.listen(bundle.listenPort, bindHost, () => {
|
|
77
|
+
server.removeListener("error", reject);
|
|
78
|
+
resolve();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
// Marker is written LAST: its presence means the proxy is accepting
|
|
82
|
+
// connections, so the control plane can start the sandbox without a race.
|
|
83
|
+
writeFileSync(join(sharedDir, "ready"), "", { mode: 0o644 });
|
|
84
|
+
const stop = async () => {
|
|
85
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
86
|
+
await disposeMitmCA(ca);
|
|
87
|
+
};
|
|
88
|
+
return { server, port: bundle.listenPort, ca, stop };
|
|
89
|
+
}
|
|
90
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
91
|
+
startEgressProxySidecar(process.env)
|
|
92
|
+
.then((running) => {
|
|
93
|
+
const shutdown = () => {
|
|
94
|
+
void running.stop().then(() => process.exit(0));
|
|
95
|
+
};
|
|
96
|
+
process.on("SIGTERM", shutdown);
|
|
97
|
+
process.on("SIGINT", shutdown);
|
|
98
|
+
})
|
|
99
|
+
.catch((error) => {
|
|
100
|
+
log.error("egress_proxy_sidecar_failed", { error });
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
|
103
|
+
}
|
package/dist/src/main.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appliance entrypoint (plan 0115, Arc A slice 1 of the 0114 roadmap).
|
|
3
|
+
*
|
|
4
|
+
* One command boots a durable, authenticated OMA server with sensible
|
|
5
|
+
* defaults; the first boot mints the initial workspace API key and prints it
|
|
6
|
+
* exactly once. Every default is an override, not a requirement:
|
|
7
|
+
*
|
|
8
|
+
* OMA_HOME data directory (default ~/.oma)
|
|
9
|
+
* OMA_SQLITE_PATH explicit DB path (overrides OMA_HOME derivation)
|
|
10
|
+
* OMA_FILE_STORAGE_ROOT explicit object root (ditto)
|
|
11
|
+
* OMA_AUTH_MODE default api-key; "disabled" is respected
|
|
12
|
+
* OMA_PORT default 4180 (0 = ephemeral, for tests)
|
|
13
|
+
* OMA_HOST bind address, default 127.0.0.1
|
|
14
|
+
*
|
|
15
|
+
* Run directly (`node --experimental-transform-types src/main.ts`) or via
|
|
16
|
+
* `oma up`, whose CLI launcher adds the Node flags and selects a sandbox.
|
|
17
|
+
*/
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { pathToFileURL } from "node:url";
|
|
21
|
+
import { serve } from "@hono/node-server";
|
|
22
|
+
import { createDeploymentControlPlane, } from "./control-plane/app.js";
|
|
23
|
+
import { DEFAULT_WORKSPACE_ID } from "./control-plane/workspace.js";
|
|
24
|
+
export const DEFAULT_APPLIANCE_PORT = 4180;
|
|
25
|
+
// Pure so tests can assert the derivation. OMA_HOME only fills the storage
|
|
26
|
+
// pair when neither is set explicitly; setting exactly one of the pair still
|
|
27
|
+
// hits the existing "must be set together" error from deployment storage.
|
|
28
|
+
export function resolveApplianceEnv(env) {
|
|
29
|
+
const resolved = { ...env };
|
|
30
|
+
if (resolved.OMA_SQLITE_PATH === undefined &&
|
|
31
|
+
resolved.OMA_FILE_STORAGE_ROOT === undefined) {
|
|
32
|
+
const home = resolved.OMA_HOME ?? join(homedir(), ".oma");
|
|
33
|
+
resolved.OMA_SQLITE_PATH = join(home, "oma.sqlite");
|
|
34
|
+
resolved.OMA_FILE_STORAGE_ROOT = join(home, "files");
|
|
35
|
+
}
|
|
36
|
+
resolved.OMA_AUTH_MODE ??= "api-key";
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
export function parseAppliancePort(raw) {
|
|
40
|
+
if (raw === undefined)
|
|
41
|
+
return DEFAULT_APPLIANCE_PORT;
|
|
42
|
+
const port = Number.parseInt(raw, 10);
|
|
43
|
+
if (!Number.isSafeInteger(port) || port < 0 || port > 65535 || String(port) !== raw.trim()) {
|
|
44
|
+
throw new Error(`Invalid OMA_PORT: ${JSON.stringify(raw)} (expected an integer 0-65535)`);
|
|
45
|
+
}
|
|
46
|
+
return port;
|
|
47
|
+
}
|
|
48
|
+
export async function startAppliance(env = process.env, opts = {}) {
|
|
49
|
+
const log = opts.log ?? console.log;
|
|
50
|
+
const resolved = resolveApplianceEnv(env);
|
|
51
|
+
const port = parseAppliancePort(resolved.OMA_PORT);
|
|
52
|
+
const host = resolved.OMA_HOST ?? "127.0.0.1";
|
|
53
|
+
const plane = createDeploymentControlPlane(resolved);
|
|
54
|
+
const { app, stores, authMode } = plane;
|
|
55
|
+
let server;
|
|
56
|
+
try {
|
|
57
|
+
const bound = await new Promise((resolvePromise, reject) => {
|
|
58
|
+
const onError = (error) => reject(error);
|
|
59
|
+
const s = serve({ fetch: app.fetch, hostname: host, port }, (info) => {
|
|
60
|
+
s.off("error", onError);
|
|
61
|
+
resolvePromise({ server: s, boundPort: info.port });
|
|
62
|
+
});
|
|
63
|
+
s.on("error", onError);
|
|
64
|
+
});
|
|
65
|
+
server = bound.server;
|
|
66
|
+
const boundPort = bound.boundPort;
|
|
67
|
+
// Mint only after the server has bound. First boot = no key was ever
|
|
68
|
+
// minted (revoked tombstones count as minted), so persisting a key on a
|
|
69
|
+
// boot that then fails to bind (EADDRINUSE) would make every later boot
|
|
70
|
+
// skip minting — an unprinted key locking the operator out. A key that
|
|
71
|
+
// prints but never serves only costs a retry; the reverse costs the
|
|
72
|
+
// quickstart.
|
|
73
|
+
const minted = authMode === "api-key" && stores.workspaces.countApiKeys() === 0
|
|
74
|
+
? stores.workspaces.mintKey(DEFAULT_WORKSPACE_ID, "first-boot")
|
|
75
|
+
: undefined;
|
|
76
|
+
const baseUrl = `http://${host}:${boundPort}`;
|
|
77
|
+
log(`open-managed-agents listening on ${baseUrl}`);
|
|
78
|
+
log(` console: ${baseUrl}/console/`);
|
|
79
|
+
log(` api: ${baseUrl}`);
|
|
80
|
+
log(` data: ${resolved.OMA_SQLITE_PATH} | files: ${resolved.OMA_FILE_STORAGE_ROOT}`);
|
|
81
|
+
log(` auth: ${authMode}`);
|
|
82
|
+
if (minted !== undefined) {
|
|
83
|
+
log("");
|
|
84
|
+
log(`First boot: minted the initial API key for ${minted.workspaceId}.`);
|
|
85
|
+
log("It is shown once and stored only as a hash — save it now:");
|
|
86
|
+
log("");
|
|
87
|
+
log(` x-api-key: ${minted.plaintextKey}`);
|
|
88
|
+
log("");
|
|
89
|
+
log(`Connect any Anthropic SDK client with base URL ${baseUrl} and that x-api-key.`);
|
|
90
|
+
// tsx is a devDependency and absent from the Docker image; the node
|
|
91
|
+
// flag works in both the checkout and the container.
|
|
92
|
+
log("Mint more keys/workspaces: node --experimental-transform-types scripts/oma-workspaces.ts --help");
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
port: boundPort,
|
|
96
|
+
close: async () => {
|
|
97
|
+
await closeServer(bound.server);
|
|
98
|
+
await plane.close();
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// Failed startups must release everything (notably .oma.lock) so the
|
|
104
|
+
// operator's retry is a clean boot, not a lock error.
|
|
105
|
+
if (server !== undefined) {
|
|
106
|
+
await closeServer(server).catch(() => { });
|
|
107
|
+
}
|
|
108
|
+
await plane.close();
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function closeServer(server) {
|
|
113
|
+
return new Promise((resolvePromise, reject) => {
|
|
114
|
+
server.close((err) => (err ? reject(err) : resolvePromise()));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
118
|
+
const appliance = await startAppliance(process.env, {});
|
|
119
|
+
const shutdown = () => {
|
|
120
|
+
void appliance.close().then(() => process.exit(0));
|
|
121
|
+
};
|
|
122
|
+
process.once("SIGINT", shutdown);
|
|
123
|
+
process.once("SIGTERM", shutdown);
|
|
124
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed Agents event types — the wire shapes we publish to API clients.
|
|
3
|
+
*
|
|
4
|
+
* The `type` strings are Tier 1 wire-compatible per ADR 0004 — they must match
|
|
5
|
+
* Anthropic's Managed Agents API verbatim. Per-type fields are deliberately
|
|
6
|
+
* modeled as an open object at this layer; typed elaboration lives in the
|
|
7
|
+
* route/translator layer where the discriminator gives us the variant.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Discriminator strings emitted by the platform. MVP subset; extend as needed.
|
|
11
|
+
*
|
|
12
|
+
* Declared as a runtime `as const` array so we can iterate it in alignment
|
|
13
|
+
* tests (see `src/types/__tests__/events.test.ts`); the TS union is derived
|
|
14
|
+
* via `typeof[number]` so the compile-time and runtime views can never drift.
|
|
15
|
+
*/
|
|
16
|
+
export const EVENT_TYPES = [
|
|
17
|
+
// Agent-originated
|
|
18
|
+
"agent.message",
|
|
19
|
+
"agent.tool_use",
|
|
20
|
+
"agent.tool_result",
|
|
21
|
+
"agent.custom_tool_use",
|
|
22
|
+
"agent.mcp_tool_use",
|
|
23
|
+
"agent.mcp_tool_result",
|
|
24
|
+
// Span observability
|
|
25
|
+
"span.model_request_start",
|
|
26
|
+
"span.model_request_end",
|
|
27
|
+
// Session lifecycle
|
|
28
|
+
"session.status_running",
|
|
29
|
+
"session.status_idle",
|
|
30
|
+
"session.status_rescheduled",
|
|
31
|
+
"session.status_terminated",
|
|
32
|
+
"session.deleted",
|
|
33
|
+
"session.error",
|
|
34
|
+
// User-originated (echoed back on the stream after server processes them)
|
|
35
|
+
"user.message",
|
|
36
|
+
"user.interrupt",
|
|
37
|
+
"user.custom_tool_result",
|
|
38
|
+
"user.tool_confirmation",
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Generate a UUIDv7 — time-ordered 128-bit ID with a 48-bit millisecond
|
|
42
|
+
* timestamp prefix. Used so lexical sort = chronological sort, which makes
|
|
43
|
+
* `WHERE id > ? ORDER BY id` a valid cursor scan in SQLite.
|
|
44
|
+
*
|
|
45
|
+
* Strict intra-process monotonicity via **counter-based** scheme (RFC 9562
|
|
46
|
+
* §6.2 Method 1, with random seeding per Method 3): when multiple IDs are
|
|
47
|
+
* minted in the same millisecond, a 12-bit counter in `rand_a` advances
|
|
48
|
+
* deterministically. This preserves real-clock fidelity in the embedded `ms`
|
|
49
|
+
* field — code-review MEDIUM 4 finding fixed (no more timestamp inflation).
|
|
50
|
+
*
|
|
51
|
+
* Layout (per RFC 9562 §5.7):
|
|
52
|
+
* bits 0-47 unix_ts_ms (48-bit ms)
|
|
53
|
+
* bits 48-51 ver (constant 0b0111)
|
|
54
|
+
* bits 52-63 counter (12-bit monotonic within an ms)
|
|
55
|
+
* bits 64-65 var (constant 0b10)
|
|
56
|
+
* bits 66-127 random (62 bits)
|
|
57
|
+
*
|
|
58
|
+
* Counter behavior:
|
|
59
|
+
* - When `ms` advances: counter seeded with random 0..2047 (leaves 2048
|
|
60
|
+
* increments of headroom within the ms; mitigates cross-process collisions)
|
|
61
|
+
* - When `ms` is equal or earlier (clock tied or rewound): counter bumps;
|
|
62
|
+
* if it overflows 0xFFF we borrow 1ms (this is the only path that inflates
|
|
63
|
+
* timestamps, and only by 1ms per 4096-ID burst within a single ms)
|
|
64
|
+
*
|
|
65
|
+
* Spec: https://datatracker.ietf.org/doc/rfc9562/ §5.7 + §6.2
|
|
66
|
+
*/
|
|
67
|
+
let lastMs = 0n;
|
|
68
|
+
let lastCounter = 0;
|
|
69
|
+
export function uuidv7() {
|
|
70
|
+
let ms = BigInt(Date.now());
|
|
71
|
+
let counter;
|
|
72
|
+
if (ms > lastMs) {
|
|
73
|
+
lastMs = ms;
|
|
74
|
+
// Seed counter with random 0..2047 (top bit clear → 2048 increments
|
|
75
|
+
// available before counter overflow within the same ms).
|
|
76
|
+
counter = crypto.getRandomValues(new Uint16Array(1))[0] & 0x07ff;
|
|
77
|
+
lastCounter = counter;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// Same ms (or rewound clock). Reuse lastMs; bump counter.
|
|
81
|
+
ms = lastMs;
|
|
82
|
+
lastCounter += 1;
|
|
83
|
+
if (lastCounter > 0x0fff) {
|
|
84
|
+
// 12-bit counter overflow. Borrow 1ms (only timestamp-inflation path).
|
|
85
|
+
lastMs += 1n;
|
|
86
|
+
ms = lastMs;
|
|
87
|
+
counter = crypto.getRandomValues(new Uint16Array(1))[0] & 0x07ff;
|
|
88
|
+
lastCounter = counter;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
counter = lastCounter;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const random = crypto.getRandomValues(new Uint8Array(8));
|
|
95
|
+
const b0 = Number((ms >> 40n) & 0xffn);
|
|
96
|
+
const b1 = Number((ms >> 32n) & 0xffn);
|
|
97
|
+
const b2 = Number((ms >> 24n) & 0xffn);
|
|
98
|
+
const b3 = Number((ms >> 16n) & 0xffn);
|
|
99
|
+
const b4 = Number((ms >> 8n) & 0xffn);
|
|
100
|
+
const b5 = Number(ms & 0xffn);
|
|
101
|
+
// version 7 (high nibble) | top 4 bits of 12-bit counter (low nibble)
|
|
102
|
+
const b6 = 0x70 | ((counter >> 8) & 0x0f);
|
|
103
|
+
// low 8 bits of 12-bit counter
|
|
104
|
+
const b7 = counter & 0xff;
|
|
105
|
+
// variant 0b10 (high 2 bits) | 6 random bits
|
|
106
|
+
const b8 = 0x80 | (random[0] & 0x3f);
|
|
107
|
+
const b9 = random[1];
|
|
108
|
+
const tail = random.slice(2); // 6 random bytes
|
|
109
|
+
const hex = (n) => n.toString(16).padStart(2, "0");
|
|
110
|
+
return [
|
|
111
|
+
hex(b0) + hex(b1) + hex(b2) + hex(b3),
|
|
112
|
+
hex(b4) + hex(b5),
|
|
113
|
+
hex(b6) + hex(b7),
|
|
114
|
+
hex(b8) + hex(b9),
|
|
115
|
+
[...tail].map(hex).join(""),
|
|
116
|
+
].join("-");
|
|
117
|
+
}
|
|
118
|
+
/** Convenience: prefixed event ID. */
|
|
119
|
+
export function newEventId() {
|
|
120
|
+
return `sevt_${uuidv7()}`;
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|