@inetafrica/open-claudia 2.14.8 → 3.0.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/.env.example +30 -4
- package/CHANGELOG.md +21 -0
- package/README.md +87 -66
- package/bin/agent.js +44 -18
- package/bin/cli.js +30 -28
- package/bin/dream.js +5 -11
- package/bin/loopback-client.js +29 -5
- package/bin/schedule.js +19 -5
- package/bin/tool.js +23 -5
- package/bot-agent.js +5 -2350
- package/bot.js +105 -7
- package/channels/telegram/adapter.js +6 -1
- package/channels/voice/adapter.js +1 -0
- package/channels/voice/manage.js +43 -5
- package/config-dir.js +3 -11
- package/core/actions.js +149 -40
- package/core/approvals.js +136 -29
- package/core/auth-flow.js +103 -19
- package/core/config-dir.js +19 -0
- package/core/config.js +127 -69
- package/core/doctor.js +111 -57
- package/core/dream.js +219 -62
- package/core/enforcer.js +0 -0
- package/core/entities.js +2 -1
- package/core/fsutil.js +55 -0
- package/core/handlers.js +503 -217
- package/core/ideas.js +2 -1
- package/core/identity.js +8 -11
- package/core/io.js +24 -3
- package/core/jobs.js +589 -72
- package/core/lessons.js +3 -2
- package/core/loopback.js +45 -8
- package/core/memory-mutation-queue.js +241 -0
- package/core/migration-backup.js +1060 -0
- package/core/pack-review.js +295 -88
- package/core/packs.js +4 -3
- package/core/persona.js +2 -1
- package/core/process-tree.js +19 -2
- package/core/provider-migration.js +39 -0
- package/core/provider-status.js +80 -0
- package/core/providers/claude-events.js +117 -0
- package/core/providers/claude-hook.js +114 -0
- package/core/providers/claude.js +296 -0
- package/core/providers/codex-events.js +121 -0
- package/core/providers/codex-hook.js +280 -0
- package/core/providers/codex.js +286 -0
- package/core/providers/contract.js +153 -0
- package/core/providers/env.js +148 -0
- package/core/providers/events.js +170 -0
- package/core/providers/hook-command.js +22 -0
- package/core/providers/index.js +240 -0
- package/core/providers/utility-policy.js +269 -0
- package/core/recall/classic.js +2 -2
- package/core/recall/discoverer.js +71 -22
- package/core/recall/metrics.js +27 -1
- package/core/recall/tuning.js +4 -1
- package/core/recall/warm-walker.js +151 -108
- package/core/recall-filter.js +86 -61
- package/core/router.js +79 -7
- package/core/run-context.js +185 -0
- package/core/runner.js +1415 -1282
- package/core/scheduler.js +515 -210
- package/core/side-chat.js +346 -0
- package/core/state.js +1096 -95
- package/core/subagent.js +72 -98
- package/core/system-prompt.js +462 -279
- package/core/tool-guard.js +73 -39
- package/core/tools.js +22 -5
- package/core/transcripts.js +30 -1
- package/core/usage-log.js +19 -4
- package/core/utility-agent.js +654 -0
- package/core/web-auth.js +29 -13
- package/core/web-sessions.js +78 -0
- package/docs/CHANNEL_DESIGN.md +181 -0
- package/docs/MULTI_CHANNEL_PLAN.md +254 -0
- package/docs/PROVIDER_MIGRATION.md +67 -0
- package/health.js +50 -39
- package/package.json +51 -4
- package/setup.js +198 -38
- package/soul.md +39 -0
- package/test-ability-extraction.js +5 -0
- package/test-approval-async.js +63 -4
- package/test-cursor-removal.js +334 -0
- package/test-delivery-contract.js +88 -0
- package/test-enforcer.js +70 -27
- package/test-fixtures/current-provider-parity.json +132 -0
- package/test-fixtures/fake-agent-cli.js +477 -0
- package/test-fixtures/migrations/claude-only/jobs.json +3 -0
- package/test-fixtures/migrations/claude-only/sessions.json +5 -0
- package/test-fixtures/migrations/claude-only/state.json +5 -0
- package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
- package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
- package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
- package/test-fixtures/migrations/cursor-selected/state.json +6 -0
- package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
- package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
- package/test-fixtures/migrations/dual-provider/state.json +10 -0
- package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
- package/test-fixtures/migrations/malformed-partial/state.json +1 -0
- package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
- package/test-fixtures/migrations/missing-project/jobs.json +3 -0
- package/test-fixtures/migrations/missing-project/sessions.json +3 -0
- package/test-fixtures/migrations/missing-project/state.json +4 -0
- package/test-fixtures/migrations/multi-user/jobs.json +4 -0
- package/test-fixtures/migrations/multi-user/sessions.json +4 -0
- package/test-fixtures/migrations/multi-user/state.json +6 -0
- package/test-fixtures/provider-event-samples.json +17 -0
- package/test-learning-e2e.js +5 -0
- package/test-memory-mutation-queue.js +191 -0
- package/test-provider-boot-matrix.js +399 -0
- package/test-provider-bootstrap.js +158 -0
- package/test-provider-capabilities.js +232 -0
- package/test-provider-claude.js +206 -0
- package/test-provider-codex.js +228 -0
- package/test-provider-compaction.js +371 -0
- package/test-provider-core-prompt.js +264 -0
- package/test-provider-coupling-audit.js +90 -0
- package/test-provider-dream.js +312 -0
- package/test-provider-enforcer.js +252 -0
- package/test-provider-env-isolation.js +150 -0
- package/test-provider-events.js +141 -0
- package/test-provider-fixture.js +332 -0
- package/test-provider-gateway.js +508 -0
- package/test-provider-language.js +89 -0
- package/test-provider-migration-backup.js +424 -0
- package/test-provider-pack-review.js +436 -0
- package/test-provider-parity-e2e.js +537 -0
- package/test-provider-prompt-parity.js +89 -0
- package/test-provider-recall.js +271 -0
- package/test-provider-registry.js +251 -0
- package/test-provider-resume-parity.js +41 -0
- package/test-provider-run-lifecycle.js +349 -0
- package/test-provider-runner.js +271 -0
- package/test-provider-scheduler.js +689 -0
- package/test-provider-session-commands.js +185 -0
- package/test-provider-session-history.js +205 -0
- package/test-provider-side-chat.js +337 -0
- package/test-provider-state-migration.js +316 -0
- package/test-provider-stream-decoder.js +69 -0
- package/test-provider-subagent.js +228 -0
- package/test-provider-tool-hooks.js +360 -0
- package/test-recall-discoverer.js +1 -0
- package/test-recall-engine.js +3 -3
- package/test-recall-evolution.js +18 -0
- package/test-run-lock.js +63 -0
- package/test-runner-watchdog-static.js +16 -8
- package/test-single-runtime.js +35 -0
- package/test-telegram-poll-recovery.js +56 -0
- package/test-tool-guard.js +56 -0
- package/test-tools.js +19 -0
- package/test-unified-identity.js +3 -1
- package/test-usage-accounting.js +92 -20
- package/test-utility-provider-policy.js +486 -0
- package/test-web-sessions.js +74 -0
- package/web.js +159 -45
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const RESULT_PREFIX = "__PROVIDER_ENFORCER_RESULT__";
|
|
12
|
+
|
|
13
|
+
function isolate(label) {
|
|
14
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), `open-claudia-enforcer-${label}-`));
|
|
15
|
+
process.env.OPEN_CLAUDIA_TEST = "1";
|
|
16
|
+
process.env.OPEN_CLAUDIA_CONFIG_DIR = path.join(root, "config");
|
|
17
|
+
process.env.HOME = root;
|
|
18
|
+
process.env.WORKSPACE = root;
|
|
19
|
+
fs.mkdirSync(process.env.OPEN_CLAUDIA_CONFIG_DIR, { recursive: true });
|
|
20
|
+
return root;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeProvider(kind, root, controls) {
|
|
24
|
+
const fixture = path.join(__dirname, "test-fixtures", "fake-agent-cli.js");
|
|
25
|
+
const { createClaudeProvider } = require("./core/providers/claude");
|
|
26
|
+
const { createCodexProvider } = require("./core/providers/codex");
|
|
27
|
+
const createProvider = kind === "claude" ? createClaudeProvider : createCodexProvider;
|
|
28
|
+
const provider = createProvider({
|
|
29
|
+
hookTransport: false,
|
|
30
|
+
resolveExecutable: () => fixture,
|
|
31
|
+
defaultModel: `${kind}-default-fixture`,
|
|
32
|
+
buildEnv: () => ({ PATH: process.env.PATH || "", HOME: root, FAKE_AGENT_KIND: kind }),
|
|
33
|
+
getAuthStatus: () => ({ state: "authenticated", reason: null }),
|
|
34
|
+
});
|
|
35
|
+
const base = provider.buildUtilityInvocation;
|
|
36
|
+
const captures = [];
|
|
37
|
+
provider.buildUtilityInvocation = (context) => {
|
|
38
|
+
const invocation = base(context);
|
|
39
|
+
const control = controls.shift() || { FAKE_AGENT_TEXT: '{"decision":"allow","reason":"default fixture"}' };
|
|
40
|
+
const capture = path.join(root, `${kind}-${captures.length}-${context.purpose}.json`);
|
|
41
|
+
captures.push(capture);
|
|
42
|
+
Object.assign(invocation.env, control, { FAKE_AGENT_CAPTURE: capture });
|
|
43
|
+
return invocation;
|
|
44
|
+
};
|
|
45
|
+
return { provider, captures, fixture };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function registry(providerEntries, states = {}) {
|
|
49
|
+
const map = Object.fromEntries(providerEntries.map((entry) => [entry.provider.id, entry]));
|
|
50
|
+
return {
|
|
51
|
+
getProvider(id) {
|
|
52
|
+
if (!map[id]) throw Object.assign(new Error(`unknown provider ${id}`), { code: "UNKNOWN_PROVIDER" });
|
|
53
|
+
return map[id].provider;
|
|
54
|
+
},
|
|
55
|
+
listProviders: () => providerEntries.map((entry) => entry.provider),
|
|
56
|
+
providerStatus(id) {
|
|
57
|
+
if (!map[id]) throw Object.assign(new Error(`unknown provider ${id}`), { code: "UNKNOWN_PROVIDER" });
|
|
58
|
+
const state = states[id] || {};
|
|
59
|
+
return {
|
|
60
|
+
id,
|
|
61
|
+
availability: state.availability || { state: "available", executable: map[id].fixture, reason: null },
|
|
62
|
+
compatibility: state.compatibility || { state: "compatible", reason: null },
|
|
63
|
+
auth: state.auth || { state: "authenticated", reason: null },
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function auditSink() {
|
|
70
|
+
const entries = [];
|
|
71
|
+
return { entries, log(kind, payload) { const entry = { kind, ...payload }; entries.push(entry); return entry; } };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function providerProbe(kind) {
|
|
75
|
+
const root = isolate(kind);
|
|
76
|
+
const timeoutSignal = path.join(root, `${kind}-timeout-signal.json`);
|
|
77
|
+
const controls = [
|
|
78
|
+
{ FAKE_AGENT_TEXT: '{"decision":"allow","reason":"inside mandate"}' },
|
|
79
|
+
{ FAKE_AGENT_TEXT: '{"decision":"block","reason":"outside mandate"}' },
|
|
80
|
+
{ FAKE_AGENT_TEXT: '{"decision":"escalate","reason":"owner must decide"}' },
|
|
81
|
+
{ FAKE_AGENT_TEXT: "not-json" },
|
|
82
|
+
{ FAKE_AGENT_SCENARIO: "forced-termination", FAKE_AGENT_SIGNAL_CAPTURE: timeoutSignal },
|
|
83
|
+
];
|
|
84
|
+
const entry = makeProvider(kind, root, controls);
|
|
85
|
+
const model = `${kind}-enforcer-exact`;
|
|
86
|
+
const { createUtilityAgentService } = require("./core/utility-agent");
|
|
87
|
+
const utility = createUtilityAgentService({
|
|
88
|
+
registry: registry([entry]),
|
|
89
|
+
env: { ENFORCER_PROVIDER: kind, [`ENFORCER_MODEL_${kind.toUpperCase()}`]: model },
|
|
90
|
+
killGraceMs: 30,
|
|
91
|
+
});
|
|
92
|
+
const enforcer = require("./core/enforcer");
|
|
93
|
+
assert.strictEqual(typeof enforcer.createEnforcerService, "function", "enforcer exposes an injectable utility-provider service");
|
|
94
|
+
const audit = auditSink();
|
|
95
|
+
const service = enforcer.createEnforcerService({
|
|
96
|
+
spawnUtilityAgent: utility.spawnUtilityAgent,
|
|
97
|
+
audit,
|
|
98
|
+
timeoutMs: 2000,
|
|
99
|
+
});
|
|
100
|
+
const speaker = { name: "External Fixture", relationship: "external", mandate: "May discuss invoice status only." };
|
|
101
|
+
const secret = ["sk", "proj", "guard", "fixture", "private"].join("-");
|
|
102
|
+
const allow = await service.vetReply(speaker, `Invoice status is paid. ${secret}`);
|
|
103
|
+
const block = await service.vetReply(speaker, "Here are internal infrastructure details.");
|
|
104
|
+
const escalate = await service.vetAction(speaker, { command: "open-claudia tool run billing change-owner", tier: "write" });
|
|
105
|
+
const malformed = await service.vetReply(speaker, "malformed verdict fixture");
|
|
106
|
+
const timeoutService = enforcer.createEnforcerService({
|
|
107
|
+
spawnUtilityAgent: utility.spawnUtilityAgent,
|
|
108
|
+
audit,
|
|
109
|
+
timeoutMs: 140,
|
|
110
|
+
});
|
|
111
|
+
const timedOut = await timeoutService.vetReply(speaker, "timeout verdict fixture");
|
|
112
|
+
|
|
113
|
+
assert.strictEqual(allow.decision, "allow");
|
|
114
|
+
assert.strictEqual(block.decision, "block");
|
|
115
|
+
assert.strictEqual(escalate.decision, "escalate");
|
|
116
|
+
for (const verdict of [allow, block, escalate]) {
|
|
117
|
+
assert.strictEqual(verdict.provider, kind);
|
|
118
|
+
assert.strictEqual(verdict.model, model);
|
|
119
|
+
assert.strictEqual(verdict.fallbackUsed, false);
|
|
120
|
+
assert.strictEqual(verdict.status, "judged");
|
|
121
|
+
}
|
|
122
|
+
for (const failure of [malformed, timedOut]) {
|
|
123
|
+
assert.strictEqual(failure.decision, "escalate", "guard failures always fail closed");
|
|
124
|
+
assert.strictEqual(failure.provider, kind);
|
|
125
|
+
assert.strictEqual(failure.model, model);
|
|
126
|
+
assert.strictEqual(failure.status, "failed");
|
|
127
|
+
assert.match(failure.reason, /failing closed/);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
assert.strictEqual(audit.entries.length, 5);
|
|
131
|
+
assert.ok(audit.entries.every((item) => item.kind === "enforcer.verdict"));
|
|
132
|
+
assert.ok(audit.entries.every((item) => item.provider === kind && item.model === model));
|
|
133
|
+
const serializedAudit = JSON.stringify(audit.entries);
|
|
134
|
+
assert.ok(!serializedAudit.includes(secret), "audit metadata never contains the judged prompt or payload");
|
|
135
|
+
assert.ok(!serializedAudit.includes("mandate"), "audit metadata never contains the private mandate");
|
|
136
|
+
|
|
137
|
+
assert.strictEqual(entry.captures.length, 5);
|
|
138
|
+
for (const capture of entry.captures) {
|
|
139
|
+
const raw = fs.readFileSync(capture, "utf8");
|
|
140
|
+
const record = JSON.parse(raw);
|
|
141
|
+
assert.ok(!raw.includes(secret), "fixture capture contains stdin metadata, never the guarded payload");
|
|
142
|
+
assert.strictEqual(record.stdin.present, true);
|
|
143
|
+
assert.ok(record.argv.includes(model));
|
|
144
|
+
assert.ok(record.argv.includes(kind === "claude" ? "plan" : "read-only"));
|
|
145
|
+
assert.ok(record.argv.includes(kind === "claude" ? "--json-schema" : "--output-schema"));
|
|
146
|
+
}
|
|
147
|
+
assert.strictEqual(JSON.parse(fs.readFileSync(timeoutSignal, "utf8")).signal, "SIGTERM");
|
|
148
|
+
|
|
149
|
+
return { kind, decisions: [allow.decision, block.decision, escalate.decision, malformed.decision, timedOut.decision] };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function fallbackProbe() {
|
|
153
|
+
const root = isolate("fallback");
|
|
154
|
+
const claude = makeProvider("claude", root, [{ FAKE_AGENT_SCENARIO: "auth-failure" }]);
|
|
155
|
+
const codex = makeProvider("codex", root, [{ FAKE_AGENT_TEXT: '{"decision":"allow","reason":"fallback judged"}' }]);
|
|
156
|
+
const { createUtilityAgentService } = require("./core/utility-agent");
|
|
157
|
+
const fallbackUtility = createUtilityAgentService({
|
|
158
|
+
registry: registry([claude, codex]),
|
|
159
|
+
env: {
|
|
160
|
+
ENFORCER_PROVIDER: "claude",
|
|
161
|
+
PROVIDER_FALLBACKS: "codex",
|
|
162
|
+
ENFORCER_MODEL_CLAUDE: "claude-enforcer-exact",
|
|
163
|
+
ENFORCER_MODEL_CODEX: "codex-enforcer-exact",
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
const enforcer = require("./core/enforcer");
|
|
167
|
+
const fallbackAudit = auditSink();
|
|
168
|
+
const fallbackService = enforcer.createEnforcerService({ spawnUtilityAgent: fallbackUtility.spawnUtilityAgent, audit: fallbackAudit });
|
|
169
|
+
const speaker = { relationship: "external", mandate: "May discuss invoice status." };
|
|
170
|
+
const fallback = await fallbackService.vetReply(speaker, "fallback invoice fixture");
|
|
171
|
+
assert.strictEqual(fallback.decision, "allow");
|
|
172
|
+
assert.strictEqual(fallback.provider, "codex");
|
|
173
|
+
assert.strictEqual(fallback.model, "codex-enforcer-exact");
|
|
174
|
+
assert.strictEqual(fallback.preferredProvider, "claude");
|
|
175
|
+
assert.strictEqual(fallback.fallbackUsed, true);
|
|
176
|
+
assert.strictEqual(fallbackAudit.entries[0].provider, "codex");
|
|
177
|
+
assert.strictEqual(fallbackAudit.entries[0].fallbackUsed, true);
|
|
178
|
+
assert.strictEqual(claude.captures.length, 1, "preferred provider was attempted first");
|
|
179
|
+
assert.strictEqual(codex.captures.length, 1, "only the explicitly configured fallback judged after auth failure");
|
|
180
|
+
|
|
181
|
+
const noFallbackCodex = makeProvider("codex", root, []);
|
|
182
|
+
const noFallbackUtility = createUtilityAgentService({
|
|
183
|
+
registry: registry([claude, noFallbackCodex], {
|
|
184
|
+
claude: { availability: { state: "missing", executable: null, reason: "missing fixture" } },
|
|
185
|
+
}),
|
|
186
|
+
env: { ENFORCER_PROVIDER: "claude" },
|
|
187
|
+
});
|
|
188
|
+
const noFallbackService = enforcer.createEnforcerService({ spawnUtilityAgent: noFallbackUtility.spawnUtilityAgent, audit: auditSink() });
|
|
189
|
+
const noFallback = await noFallbackService.vetReply(speaker, "no implicit fallback fixture");
|
|
190
|
+
assert.strictEqual(noFallback.decision, "escalate");
|
|
191
|
+
assert.strictEqual(noFallback.status, "failed");
|
|
192
|
+
assert.strictEqual(noFallbackCodex.captures.length, 0, "enforcer never invents an unconfigured fallback");
|
|
193
|
+
|
|
194
|
+
const failedClaude = makeProvider("claude", root, []);
|
|
195
|
+
const failedCodex = makeProvider("codex", root, []);
|
|
196
|
+
const totalFailureUtility = createUtilityAgentService({
|
|
197
|
+
registry: registry([failedClaude, failedCodex], {
|
|
198
|
+
claude: { availability: { state: "missing", executable: null, reason: "missing Claude" } },
|
|
199
|
+
codex: { auth: { state: "unauthenticated", reason: "missing Codex auth" } },
|
|
200
|
+
}),
|
|
201
|
+
env: { ENFORCER_PROVIDER: "claude", PROVIDER_FALLBACKS: "codex" },
|
|
202
|
+
});
|
|
203
|
+
const totalAudit = auditSink();
|
|
204
|
+
const totalService = enforcer.createEnforcerService({ spawnUtilityAgent: totalFailureUtility.spawnUtilityAgent, audit: totalAudit });
|
|
205
|
+
const totalFailure = await totalService.vetAction(speaker, { command: "dangerous total failure fixture", tier: "destructive" });
|
|
206
|
+
assert.strictEqual(totalFailure.decision, "escalate");
|
|
207
|
+
assert.strictEqual(totalFailure.status, "failed");
|
|
208
|
+
assert.match(totalFailure.reason, /failing closed/);
|
|
209
|
+
assert.strictEqual(totalAudit.entries[0].decision, "escalate");
|
|
210
|
+
assert.strictEqual(totalAudit.entries[0].status, "failed");
|
|
211
|
+
|
|
212
|
+
return { fallbackProvider: fallback.provider, noFallback: noFallback.decision, totalFailure: totalFailure.decision };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function runChild(args) {
|
|
216
|
+
const child = spawnSync(process.execPath, [__filename, ...args], {
|
|
217
|
+
cwd: __dirname,
|
|
218
|
+
env: { ...process.env },
|
|
219
|
+
encoding: "utf8",
|
|
220
|
+
timeout: 30000,
|
|
221
|
+
});
|
|
222
|
+
if (child.status !== 0) throw new Error(`enforcer child ${args.join(" ")} failed (${child.status})\n${child.stdout}\n${child.stderr}`);
|
|
223
|
+
const line = child.stdout.split("\n").find((entry) => entry.startsWith(RESULT_PREFIX));
|
|
224
|
+
if (!line) throw new Error(`enforcer child ${args.join(" ")} returned no result\n${child.stdout}\n${child.stderr}`);
|
|
225
|
+
return JSON.parse(line.slice(RESULT_PREFIX.length));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function main() {
|
|
229
|
+
if (process.argv[2] === "--provider") {
|
|
230
|
+
const result = await providerProbe(process.argv[3]);
|
|
231
|
+
console.log(`${RESULT_PREFIX}${JSON.stringify(result)}`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (process.argv[2] === "--fallback") {
|
|
235
|
+
const result = await fallbackProbe();
|
|
236
|
+
console.log(`${RESULT_PREFIX}${JSON.stringify(result)}`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const claude = runChild(["--provider", "claude"]);
|
|
240
|
+
const codex = runChild(["--provider", "codex"]);
|
|
241
|
+
assert.deepStrictEqual(claude.decisions, ["allow", "block", "escalate", "escalate", "escalate"]);
|
|
242
|
+
assert.deepStrictEqual(codex.decisions, claude.decisions);
|
|
243
|
+
assert.deepStrictEqual(runChild(["--fallback"]), { fallbackProvider: "codex", noFallback: "escalate", totalFailure: "escalate" });
|
|
244
|
+
const source = fs.readFileSync(path.join(__dirname, "core", "enforcer.js"), "utf8");
|
|
245
|
+
assert.ok(!source.includes("spawnSubagent"), "enforcer no longer uses the compatibility sub-agent transport");
|
|
246
|
+
console.log("provider enforcer OK");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
main().catch((error) => {
|
|
250
|
+
console.error(error.stack || error.message);
|
|
251
|
+
process.exitCode = 1;
|
|
252
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const crypto = require("crypto");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const { spawnSync } = require("child_process");
|
|
11
|
+
|
|
12
|
+
const baselinePath = path.join(__dirname, "test-fixtures", "current-provider-parity.json");
|
|
13
|
+
assert.ok(fs.existsSync(baselinePath), "current provider-parity baseline fixture must exist");
|
|
14
|
+
const baseline = JSON.parse(fs.readFileSync(baselinePath, "utf8"));
|
|
15
|
+
assert.strictEqual(baseline.environment.knownDefect, true);
|
|
16
|
+
assert.strictEqual(baseline.environment.invertInTask, 4);
|
|
17
|
+
assert.strictEqual(baseline.environment.capture, "boolean-key-presence-only");
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
NATIVE_ISOLATION_NOTICE,
|
|
21
|
+
addConfiguredPassthrough,
|
|
22
|
+
baseAgentEnv,
|
|
23
|
+
claudeAgentEnv,
|
|
24
|
+
codexAgentEnv,
|
|
25
|
+
pickStandardProcessEnv,
|
|
26
|
+
} = require("./core/providers/env");
|
|
27
|
+
|
|
28
|
+
const synthetic = Object.fromEntries(
|
|
29
|
+
baseline.environment.keys.map((key) => [key, `synthetic-canary:${key.toLowerCase()}`]),
|
|
30
|
+
);
|
|
31
|
+
const source = {
|
|
32
|
+
PATH: "/fixture/bin",
|
|
33
|
+
HOME: "/fixture/home",
|
|
34
|
+
TMPDIR: "/fixture/tmp",
|
|
35
|
+
LANG: "en_US.UTF-8",
|
|
36
|
+
TZ: "UTC",
|
|
37
|
+
SHELL: "/bin/sh",
|
|
38
|
+
PROJECT_REGION: "fixture-region",
|
|
39
|
+
AGENT_ENV_PASSTHROUGH: [
|
|
40
|
+
"PROJECT_REGION",
|
|
41
|
+
"TELEGRAM_BOT_TOKEN",
|
|
42
|
+
"OPENAI_API_KEY",
|
|
43
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
44
|
+
"OPEN_CLAUDIA_KEYRING_CANARY",
|
|
45
|
+
].join(","),
|
|
46
|
+
...synthetic,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function presence(env) {
|
|
50
|
+
return Object.fromEntries(
|
|
51
|
+
baseline.environment.keys.map((key) => [key, Object.prototype.hasOwnProperty.call(env, key)]),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hash(value) {
|
|
56
|
+
return crypto.createHash("sha256").update(String(value)).digest("hex");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const standard = pickStandardProcessEnv(source);
|
|
60
|
+
assert.deepStrictEqual(standard, {
|
|
61
|
+
PATH: source.PATH,
|
|
62
|
+
HOME: source.HOME,
|
|
63
|
+
TMPDIR: source.TMPDIR,
|
|
64
|
+
SHELL: source.SHELL,
|
|
65
|
+
LANG: source.LANG,
|
|
66
|
+
TZ: source.TZ,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const passTarget = {};
|
|
70
|
+
addConfiguredPassthrough(passTarget, source);
|
|
71
|
+
assert.deepStrictEqual(Object.keys(passTarget).sort(), [
|
|
72
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
73
|
+
"OPENAI_API_KEY",
|
|
74
|
+
"OPEN_CLAUDIA_KEYRING_CANARY",
|
|
75
|
+
"PROJECT_REGION",
|
|
76
|
+
"TELEGRAM_BOT_TOKEN",
|
|
77
|
+
].sort());
|
|
78
|
+
assert.throws(
|
|
79
|
+
() => addConfiguredPassthrough({}, { AGENT_ENV_PASSTHROUGH: "GOOD,bad-name" }),
|
|
80
|
+
(error) => error && error.code === "INVALID_ENV_PASSTHROUGH",
|
|
81
|
+
);
|
|
82
|
+
assert.throws(
|
|
83
|
+
() => addConfiguredPassthrough({}, { AGENT_ENV_PASSTHROUGH: "__proto__" }),
|
|
84
|
+
(error) => error && error.code === "INVALID_ENV_PASSTHROUGH",
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const base = baseAgentEnv(source);
|
|
88
|
+
assert.strictEqual(base.PROJECT_REGION, "fixture-region", "explicit project passthrough survives");
|
|
89
|
+
assert.deepStrictEqual(presence(base), Object.fromEntries(baseline.environment.keys.map((key) => [key, false])));
|
|
90
|
+
|
|
91
|
+
const claude = claudeAgentEnv(source);
|
|
92
|
+
const codex = codexAgentEnv(source);
|
|
93
|
+
|
|
94
|
+
// This is the Task 2 inversion: the provider environments must now be
|
|
95
|
+
// intentionally different and match the key-presence-only target fixture.
|
|
96
|
+
assert.deepStrictEqual(presence(claude), baseline.environment.target.claude);
|
|
97
|
+
assert.deepStrictEqual(presence(codex), baseline.environment.target.codex);
|
|
98
|
+
assert.notDeepStrictEqual(presence(claude), presence(codex));
|
|
99
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(claudeAgentEnv({ ...source, ANTHROPIC_API_KEY: "" }), "ANTHROPIC_API_KEY"));
|
|
100
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(codexAgentEnv({ ...source, OPENAI_API_KEY: "" }), "OPENAI_API_KEY"));
|
|
101
|
+
|
|
102
|
+
// Hash-only comparisons prove the provider-owned credentials were preserved
|
|
103
|
+
// without ever serializing or printing the values themselves.
|
|
104
|
+
assert.strictEqual(hash(claude.CLAUDE_CODE_OAUTH_TOKEN), hash(source.CLAUDE_CODE_OAUTH_TOKEN));
|
|
105
|
+
assert.strictEqual(hash(claude.ANTHROPIC_API_KEY), hash(source.ANTHROPIC_API_KEY));
|
|
106
|
+
assert.strictEqual(hash(claude.CLAUDE_CONFIG_DIR), hash(source.CLAUDE_CONFIG_DIR));
|
|
107
|
+
assert.strictEqual(hash(codex.OPENAI_API_KEY), hash(source.OPENAI_API_KEY));
|
|
108
|
+
assert.strictEqual(hash(codex.CODEX_HOME), hash(source.CODEX_HOME));
|
|
109
|
+
|
|
110
|
+
assert.match(NATIVE_ISOLATION_NOTICE, /environment isolation/i);
|
|
111
|
+
assert.match(NATIVE_ISOLATION_NOTICE, /same-UID/i);
|
|
112
|
+
assert.match(NATIVE_ISOLATION_NOTICE, /not filesystem isolation/i);
|
|
113
|
+
const runnerSource = fs.readFileSync(path.join(__dirname, "core", "runner.js"), "utf8");
|
|
114
|
+
assert.ok(!/overlay\.OC_SEND_TOKEN\s*=/.test(runnerSource), "the compatibility runner must not add the loopback token back to provider envs");
|
|
115
|
+
|
|
116
|
+
// The auth helpers are the compatibility entry points used by existing
|
|
117
|
+
// provider-specific processes until the runner switches to adapters in Task 9.
|
|
118
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-provider-env-auth-"));
|
|
119
|
+
try {
|
|
120
|
+
const configDir = path.join(root, "config");
|
|
121
|
+
const workspace = path.join(root, "workspace");
|
|
122
|
+
const probeScript = [
|
|
123
|
+
"const { claudeSubprocessEnv, codexSubprocessEnv } = require('./core/auth-flow');",
|
|
124
|
+
`const keys = ${JSON.stringify(baseline.environment.keys)};`,
|
|
125
|
+
"const presence = (env) => Object.fromEntries(keys.map((key) => [key, Object.prototype.hasOwnProperty.call(env, key)]));",
|
|
126
|
+
"process.stdout.write(JSON.stringify({ claude: presence(claudeSubprocessEnv()), codex: presence(codexSubprocessEnv()) }));",
|
|
127
|
+
].join("\n");
|
|
128
|
+
const child = spawnSync(process.execPath, ["-e", probeScript], {
|
|
129
|
+
cwd: __dirname,
|
|
130
|
+
encoding: "utf8",
|
|
131
|
+
env: {
|
|
132
|
+
...source,
|
|
133
|
+
OPEN_CLAUDIA_CONFIG_DIR: configDir,
|
|
134
|
+
WORKSPACE: workspace,
|
|
135
|
+
CLAUDE_PATH: process.execPath,
|
|
136
|
+
CODEX_PATH: process.execPath,
|
|
137
|
+
OPEN_CLAUDIA_TEST: "1",
|
|
138
|
+
},
|
|
139
|
+
timeout: 10000,
|
|
140
|
+
});
|
|
141
|
+
assert.ifError(child.error);
|
|
142
|
+
assert.strictEqual(child.status, 0, child.stderr || "auth environment probe failed");
|
|
143
|
+
assert.deepStrictEqual(JSON.parse(child.stdout), baseline.environment.target);
|
|
144
|
+
assert.ok(!fs.existsSync(configDir), "auth environment construction must not create config state");
|
|
145
|
+
assert.ok(!fs.existsSync(workspace), "auth environment construction must not create a workspace");
|
|
146
|
+
} finally {
|
|
147
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log("provider env isolation OK");
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { NORMALIZED_EVENT_TYPES, selectTerminalText } = require("./core/providers/events");
|
|
9
|
+
const { createClaudeParserState, normalizeClaudeEvent } = require("./core/providers/claude-events");
|
|
10
|
+
const { createCodexParserState, normalizeCodexEvent } = require("./core/providers/codex-events");
|
|
11
|
+
|
|
12
|
+
const samples = JSON.parse(fs.readFileSync(
|
|
13
|
+
path.join(__dirname, "test-fixtures", "provider-event-samples.json"),
|
|
14
|
+
"utf8",
|
|
15
|
+
));
|
|
16
|
+
|
|
17
|
+
assert.deepStrictEqual([...NORMALIZED_EVENT_TYPES].sort(), [
|
|
18
|
+
"error",
|
|
19
|
+
"result",
|
|
20
|
+
"session",
|
|
21
|
+
"text_delta",
|
|
22
|
+
"text_final",
|
|
23
|
+
"tool_end",
|
|
24
|
+
"tool_start",
|
|
25
|
+
"usage",
|
|
26
|
+
].sort());
|
|
27
|
+
|
|
28
|
+
const claudeState = createClaudeParserState();
|
|
29
|
+
assert.deepStrictEqual(normalizeClaudeEvent(samples.claude.session, claudeState), [
|
|
30
|
+
{ type: "session", sessionId: "claude-session-1" },
|
|
31
|
+
]);
|
|
32
|
+
assert.deepStrictEqual(normalizeClaudeEvent(samples.claude.delta, claudeState), [
|
|
33
|
+
{ type: "text_delta", text: "Hello " },
|
|
34
|
+
]);
|
|
35
|
+
assert.deepStrictEqual(normalizeClaudeEvent(samples.claude.toolUse, claudeState), [
|
|
36
|
+
{ type: "tool_start", id: "tool-1", name: "Read", detail: { file_path: "fixture.txt" } },
|
|
37
|
+
]);
|
|
38
|
+
assert.deepStrictEqual(normalizeClaudeEvent(samples.claude.toolResult, claudeState), [
|
|
39
|
+
{ type: "tool_end", id: "tool-1", name: "Read", detail: "fixture output", exitCode: 0 },
|
|
40
|
+
]);
|
|
41
|
+
assert.deepStrictEqual(normalizeClaudeEvent(samples.claude.assistant, claudeState), [
|
|
42
|
+
{ type: "text_final", text: "world" },
|
|
43
|
+
{ type: "usage", usage: { input_tokens: 12, output_tokens: 3 }, scope: "provider_call" },
|
|
44
|
+
]);
|
|
45
|
+
const claudeResult = normalizeClaudeEvent(samples.claude.result, claudeState);
|
|
46
|
+
assert.deepStrictEqual(claudeResult, [
|
|
47
|
+
{ type: "usage", usage: { input_tokens: 12, output_tokens: 3 }, scope: "turn" },
|
|
48
|
+
{ type: "result", text: "", sessionId: "claude-session-1" },
|
|
49
|
+
]);
|
|
50
|
+
assert.deepStrictEqual(normalizeClaudeEvent(samples.claude.result, claudeState), [], "duplicate Claude terminal settles once");
|
|
51
|
+
|
|
52
|
+
const claudeFallback = createClaudeParserState();
|
|
53
|
+
const fallback = normalizeClaudeEvent(samples.claude.result, claudeFallback);
|
|
54
|
+
assert.deepStrictEqual(fallback.at(-1), { type: "result", text: "Hello world", sessionId: "claude-session-1" });
|
|
55
|
+
|
|
56
|
+
const claudeStructuredState = createClaudeParserState();
|
|
57
|
+
normalizeClaudeEvent({
|
|
58
|
+
type: "assistant",
|
|
59
|
+
message: { content: [{ type: "text", text: "```json\n{\"status\":\"ok\"}\n```" }] },
|
|
60
|
+
}, claudeStructuredState);
|
|
61
|
+
const structured = normalizeClaudeEvent({
|
|
62
|
+
type: "result",
|
|
63
|
+
subtype: "success",
|
|
64
|
+
is_error: false,
|
|
65
|
+
session_id: "claude-structured-session",
|
|
66
|
+
result: "",
|
|
67
|
+
structured_output: { status: "ok" },
|
|
68
|
+
}, claudeStructuredState);
|
|
69
|
+
assert.deepStrictEqual(structured.at(-1), {
|
|
70
|
+
type: "result",
|
|
71
|
+
text: '{"status":"ok"}',
|
|
72
|
+
sessionId: "claude-structured-session",
|
|
73
|
+
});
|
|
74
|
+
assert.strictEqual(selectTerminalText({
|
|
75
|
+
resultText: '{"status":"ok"}',
|
|
76
|
+
finalText: "```json\n{\"status\":\"ok\"}\n```",
|
|
77
|
+
}), '{"status":"ok"}', "structured terminal output overrides presentation text");
|
|
78
|
+
|
|
79
|
+
const usageOnly = { type: "assistant", message: { usage: { input_tokens: 1, output_tokens: 1 }, content: [] } };
|
|
80
|
+
const usageState = createClaudeParserState();
|
|
81
|
+
assert.strictEqual(normalizeClaudeEvent(usageOnly, usageState).filter((event) => event.type === "usage").length, 1);
|
|
82
|
+
assert.strictEqual(normalizeClaudeEvent(usageOnly, usageState).filter((event) => event.type === "usage").length, 0);
|
|
83
|
+
|
|
84
|
+
const claudeAuth = normalizeClaudeEvent({
|
|
85
|
+
type: "result",
|
|
86
|
+
is_error: true,
|
|
87
|
+
subtype: "error_during_execution",
|
|
88
|
+
result: "Authentication failed",
|
|
89
|
+
errors: ["Login required"],
|
|
90
|
+
}, createClaudeParserState());
|
|
91
|
+
assert.deepStrictEqual(claudeAuth, [{
|
|
92
|
+
type: "error",
|
|
93
|
+
message: "Authentication failed",
|
|
94
|
+
authError: true,
|
|
95
|
+
usageLimit: false,
|
|
96
|
+
}]);
|
|
97
|
+
|
|
98
|
+
const codexState = createCodexParserState();
|
|
99
|
+
assert.deepStrictEqual(normalizeCodexEvent(samples.codex.session, codexState), [
|
|
100
|
+
{ type: "session", sessionId: "codex-thread-1" },
|
|
101
|
+
]);
|
|
102
|
+
assert.deepStrictEqual(normalizeCodexEvent(samples.codex.toolStart, codexState), [
|
|
103
|
+
{ type: "tool_start", id: "item-1", name: "Shell", detail: "printf fixture" },
|
|
104
|
+
]);
|
|
105
|
+
assert.deepStrictEqual(normalizeCodexEvent(samples.codex.toolEnd, codexState), [
|
|
106
|
+
{ type: "tool_end", id: "item-1", name: "Shell", detail: "fixture", exitCode: 0 },
|
|
107
|
+
]);
|
|
108
|
+
assert.deepStrictEqual(normalizeCodexEvent(samples.codex.assistant, codexState), [
|
|
109
|
+
{ type: "text_final", text: "Hello from Codex" },
|
|
110
|
+
]);
|
|
111
|
+
assert.deepStrictEqual(normalizeCodexEvent(samples.codex.result, codexState), [
|
|
112
|
+
{ type: "usage", usage: { input_tokens: 20, cached_input_tokens: 5, output_tokens: 4 }, scope: "session_total" },
|
|
113
|
+
{ type: "result", text: "", sessionId: "codex-thread-1" },
|
|
114
|
+
]);
|
|
115
|
+
assert.deepStrictEqual(normalizeCodexEvent(samples.codex.result, codexState), [], "duplicate Codex terminal and usage settle once");
|
|
116
|
+
|
|
117
|
+
const codexAuth = normalizeCodexEvent({ type: "turn.failed", error: { message: "401 Unauthorized" } }, createCodexParserState());
|
|
118
|
+
assert.deepStrictEqual(codexAuth, [{
|
|
119
|
+
type: "error",
|
|
120
|
+
message: "401 Unauthorized",
|
|
121
|
+
authError: true,
|
|
122
|
+
usageLimit: false,
|
|
123
|
+
}]);
|
|
124
|
+
|
|
125
|
+
const decoderError = { type: "decoder.error", code: "MALFORMED_JSON", message: "Malformed provider JSONL record" };
|
|
126
|
+
assert.deepStrictEqual(normalizeClaudeEvent(decoderError, createClaudeParserState()), [{
|
|
127
|
+
type: "error", message: "Malformed provider JSONL record", authError: false, usageLimit: false,
|
|
128
|
+
}]);
|
|
129
|
+
assert.deepStrictEqual(normalizeCodexEvent(decoderError, createCodexParserState()), [{
|
|
130
|
+
type: "error", message: "Malformed provider JSONL record", authError: false, usageLimit: false,
|
|
131
|
+
}]);
|
|
132
|
+
|
|
133
|
+
assert.deepStrictEqual(normalizeClaudeEvent({ type: "future.claude.event" }, createClaudeParserState()), []);
|
|
134
|
+
assert.deepStrictEqual(normalizeCodexEvent({ type: "future.codex.event" }, createCodexParserState()), []);
|
|
135
|
+
|
|
136
|
+
const isolatedA = createClaudeParserState();
|
|
137
|
+
const isolatedB = createClaudeParserState();
|
|
138
|
+
normalizeClaudeEvent(samples.claude.session, isolatedA);
|
|
139
|
+
assert.deepStrictEqual(normalizeClaudeEvent(samples.claude.session, isolatedB), [{ type: "session", sessionId: "claude-session-1" }]);
|
|
140
|
+
|
|
141
|
+
console.log("provider events OK");
|