@openape/apes 1.31.0 → 1.31.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.
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/errors.ts
4
+ var CliError = class extends Error {
5
+ constructor(message, exitCode = 1) {
6
+ super(message);
7
+ this.exitCode = exitCode;
8
+ this.name = "CliError";
9
+ }
10
+ exitCode;
11
+ };
12
+ var CliExit = class extends Error {
13
+ constructor(exitCode = 0) {
14
+ super("");
15
+ this.exitCode = exitCode;
16
+ this.name = "CliExit";
17
+ }
18
+ exitCode;
19
+ };
20
+
21
+ // src/duration.ts
22
+ function parseDuration(value) {
23
+ const match = value.match(/^(\d+)\s*([smhd])$/);
24
+ if (!match) {
25
+ throw new Error(`Invalid duration format: "${value}". Use e.g. 30m, 1h, 7d`);
26
+ }
27
+ const amount = Number.parseInt(match[1], 10);
28
+ switch (match[2]) {
29
+ case "s":
30
+ return amount;
31
+ case "m":
32
+ return amount * 60;
33
+ case "h":
34
+ return amount * 3600;
35
+ case "d":
36
+ return amount * 86400;
37
+ default:
38
+ throw new Error(`Unknown duration unit: ${match[2]}`);
39
+ }
40
+ }
41
+
42
+ // src/lib/agent-secrets-runtime.ts
43
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, watch, writeFileSync } from "fs";
44
+ import { homedir } from "os";
45
+ import { dirname, join } from "path";
46
+ import { openString } from "@openape/core";
47
+ var CONFIG_DIR = join(homedir(), ".config", "openape");
48
+ var SECRETS_DIR = join(CONFIG_DIR, "secrets.d");
49
+ var X25519_KEY_PATH = join(CONFIG_DIR, "agent-x25519.key");
50
+ var X25519_PUBKEY_PATH = `${X25519_KEY_PATH}.pub`;
51
+ function envNameFromFile(file) {
52
+ if (!file.endsWith(".blob")) return null;
53
+ const env = file.slice(0, -".blob".length);
54
+ return /^[A-Z][A-Z0-9_]*$/.test(env) ? env : null;
55
+ }
56
+ function readAgentEncryptionKey(keyPath = X25519_KEY_PATH) {
57
+ if (!existsSync(keyPath)) return null;
58
+ const k = readFileSync(keyPath, "utf8").trim();
59
+ return k.length > 0 ? k : null;
60
+ }
61
+ function readAgentEncryptionPublicKey(pubPath = X25519_PUBKEY_PATH) {
62
+ if (!existsSync(pubPath)) return null;
63
+ const k = readFileSync(pubPath, "utf8").trim();
64
+ return k.length > 0 ? k : null;
65
+ }
66
+ function materializeSecrets(opts = {}) {
67
+ const dir = opts.dir ?? SECRETS_DIR;
68
+ const env = opts.env ?? process.env;
69
+ const log = opts.log ?? (() => {
70
+ });
71
+ const applied = [];
72
+ const failed = [];
73
+ const key = readAgentEncryptionKey(opts.keyPath);
74
+ const files = key && existsSync(dir) ? readdirSync(dir) : [];
75
+ for (const file of files) {
76
+ const name = envNameFromFile(file);
77
+ if (!name) continue;
78
+ try {
79
+ const box = JSON.parse(readFileSync(join(dir, file), "utf8"));
80
+ const plaintext = openString(box, key);
81
+ const target = typeof box.materializeTo === "string" ? box.materializeTo : null;
82
+ if (target) {
83
+ const blobMtime = statSync(join(dir, file)).mtimeMs;
84
+ if (!existsSync(target) || statSync(target).mtimeMs < blobMtime) {
85
+ mkdirSync(dirname(target), { recursive: true });
86
+ writeFileSync(target, plaintext, { mode: 384 });
87
+ }
88
+ } else {
89
+ env[name] = plaintext;
90
+ }
91
+ applied.push(name);
92
+ } catch (e) {
93
+ failed.push(file);
94
+ log(`secrets: failed to open ${file}: ${e.message}`);
95
+ }
96
+ }
97
+ const live = new Set(applied);
98
+ for (const prev of opts.previouslyApplied ?? []) {
99
+ if (!live.has(prev)) {
100
+ delete env[prev];
101
+ log(`secrets: revoked ${prev}`);
102
+ }
103
+ }
104
+ return { applied, failed };
105
+ }
106
+ function startSecretsWatcher(opts = {}) {
107
+ const dir = opts.dir ?? SECRETS_DIR;
108
+ const log = opts.log ?? (() => {
109
+ });
110
+ let appliedNames = /* @__PURE__ */ new Set();
111
+ const run = () => {
112
+ const r = materializeSecrets({ ...opts, previouslyApplied: appliedNames });
113
+ appliedNames = new Set(r.applied);
114
+ };
115
+ run();
116
+ if (!existsSync(dir)) return () => {
117
+ };
118
+ let timer = null;
119
+ const watcher = watch(dir, () => {
120
+ if (timer) clearTimeout(timer);
121
+ timer = setTimeout(run, 150);
122
+ });
123
+ watcher.on("error", (err) => log(`secrets: watcher error: ${err.message}`));
124
+ return () => {
125
+ if (timer) clearTimeout(timer);
126
+ watcher.close();
127
+ };
128
+ }
129
+
130
+ export {
131
+ CliError,
132
+ CliExit,
133
+ parseDuration,
134
+ readAgentEncryptionPublicKey,
135
+ materializeSecrets,
136
+ startSecretsWatcher
137
+ };
138
+ //# sourceMappingURL=chunk-3LH4FT4R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/duration.ts","../src/lib/agent-secrets-runtime.ts"],"sourcesContent":["export class CliError extends Error {\n constructor(message: string, public exitCode: number = 1) {\n super(message)\n this.name = 'CliError'\n }\n}\n\nexport class CliExit extends Error {\n constructor(public exitCode: number = 0) {\n super('')\n this.name = 'CliExit'\n }\n}\n","/**\n * Parse a human-readable duration string into seconds.\n * Supported formats: 30s, 5m, 1h, 7d\n */\nexport function parseDuration(value: string): number {\n const match = value.match(/^(\\d+)\\s*([smhd])$/)\n if (!match) {\n throw new Error(`Invalid duration format: \"${value}\". Use e.g. 30m, 1h, 7d`)\n }\n const amount = Number.parseInt(match[1]!, 10)\n switch (match[2]) {\n case 's': return amount\n case 'm': return amount * 60\n case 'h': return amount * 3600\n case 'd': return amount * 86400\n default: throw new Error(`Unknown duration unit: ${match[2]}`)\n }\n}\n","import type { SealedBox } from '@openape/core'\nimport { existsSync, mkdirSync, readdirSync, readFileSync, statSync, watch, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\nimport { openString } from '@openape/core'\n\n// Agent-side of the capability broker. troop seals a secret to this\n// agent's X25519 pubkey (M2a/M2c); nest drops the opaque blob into\n// ~/.config/openape/secrets.d/<ENV>.blob (M2d). Here the agent — the\n// ONLY place plaintext exists — opens it with its private key and\n// injects it into process.env so its tools (bash etc.) see it. Revoke\n// = blob removed → the env var is dropped on the next materialize.\n// See plans.openape.ai 01KRTAE8 (M2e).\n\nconst CONFIG_DIR = join(homedir(), '.config', 'openape')\nexport const SECRETS_DIR = join(CONFIG_DIR, 'secrets.d')\nexport const X25519_KEY_PATH = join(CONFIG_DIR, 'agent-x25519.key')\n// Public half written alongside the private key at spawn (agent-bootstrap).\n// Reported to troop on sync so the capability broker can seal secrets to it.\nexport const X25519_PUBKEY_PATH = `${X25519_KEY_PATH}.pub`\n\nfunction envNameFromFile(file: string): string | null {\n if (!file.endsWith('.blob')) return null\n const env = file.slice(0, -'.blob'.length)\n return /^[A-Z][A-Z0-9_]*$/.test(env) ? env : null\n}\n\nexport function readAgentEncryptionKey(keyPath = X25519_KEY_PATH): string | null {\n if (!existsSync(keyPath)) return null\n const k = readFileSync(keyPath, 'utf8').trim()\n return k.length > 0 ? k : null\n}\n\nexport function readAgentEncryptionPublicKey(pubPath = X25519_PUBKEY_PATH): string | null {\n if (!existsSync(pubPath)) return null\n const k = readFileSync(pubPath, 'utf8').trim()\n return k.length > 0 ? k : null\n}\n\nexport interface MaterializeOptions {\n dir?: string\n keyPath?: string\n /** Target env map (defaults to process.env). Injected in tests. */\n env?: NodeJS.ProcessEnv\n /** Env names applied by a previous materialize, to drop on revoke. */\n previouslyApplied?: Iterable<string>\n log?: (line: string) => void\n}\n\nexport interface MaterializeResult {\n applied: string[]\n failed: string[]\n}\n\n/**\n * Open every sealed blob in the secrets dir and set the corresponding\n * env var. Any env that was applied before but whose blob is now gone\n * (revoke) is deleted. Best-effort per blob — a corrupt/foreign blob\n * is logged and skipped, never throws.\n */\nexport function materializeSecrets(opts: MaterializeOptions = {}): MaterializeResult {\n const dir = opts.dir ?? SECRETS_DIR\n const env = opts.env ?? process.env\n const log = opts.log ?? (() => {})\n const applied: string[] = []\n const failed: string[] = []\n\n const key = readAgentEncryptionKey(opts.keyPath)\n const files = key && existsSync(dir) ? readdirSync(dir) : []\n\n for (const file of files) {\n const name = envNameFromFile(file)\n if (!name) continue\n try {\n const box = JSON.parse(readFileSync(join(dir, file), 'utf8')) as SealedBox & { materializeTo?: unknown }\n const plaintext = openString(box, key!)\n const target = typeof box.materializeTo === 'string' ? box.materializeTo : null\n if (target) {\n // Seed-once: write only on first seed or a newer blob (re-verify).\n // Never clobber a file litellm refreshed in place (file > blob mtime).\n const blobMtime = statSync(join(dir, file)).mtimeMs\n if (!existsSync(target) || statSync(target).mtimeMs < blobMtime) {\n mkdirSync(dirname(target), { recursive: true })\n writeFileSync(target, plaintext, { mode: 0o600 })\n }\n }\n else {\n env[name] = plaintext\n }\n applied.push(name)\n }\n catch (e) {\n failed.push(file)\n log(`secrets: failed to open ${file}: ${(e as Error).message}`)\n }\n }\n\n // Revoke: env names we set last time but that have no blob now.\n const live = new Set(applied)\n for (const prev of opts.previouslyApplied ?? []) {\n if (!live.has(prev)) {\n delete env[prev]\n log(`secrets: revoked ${prev}`)\n }\n }\n\n return { applied, failed }\n}\n\n/**\n * Materialize once, then fs.watch the secrets dir and re-materialize\n * on any change (rotate/revoke take effect live — the user's M2 v1\n * choice). Returns a stop function. Safe to call when the dir doesn't\n * exist yet (watch attaches once it appears on the next agent start).\n */\nexport function startSecretsWatcher(opts: MaterializeOptions = {}): () => void {\n const dir = opts.dir ?? SECRETS_DIR\n const log = opts.log ?? (() => {})\n let appliedNames = new Set<string>()\n\n const run = () => {\n const r = materializeSecrets({ ...opts, previouslyApplied: appliedNames })\n appliedNames = new Set(r.applied)\n }\n\n run()\n if (!existsSync(dir)) return () => {}\n\n let timer: NodeJS.Timeout | null = null\n const watcher = watch(dir, () => {\n // Debounce — a single rotate is several fs events (create, write).\n if (timer) clearTimeout(timer)\n timer = setTimeout(run, 150)\n })\n watcher.on('error', err => log(`secrets: watcher error: ${err.message}`))\n\n return () => {\n if (timer) clearTimeout(timer)\n watcher.close()\n }\n}\n"],"mappings":"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","names":[]}