@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,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const CAPABILITY_KEYS = Object.freeze([
|
|
4
|
+
"resume",
|
|
5
|
+
"readOnlyMode",
|
|
6
|
+
"effort",
|
|
7
|
+
"budget",
|
|
8
|
+
"worktree",
|
|
9
|
+
"partialStreaming",
|
|
10
|
+
"maxTurns",
|
|
11
|
+
"imageFlag",
|
|
12
|
+
"persistentUtilityStream",
|
|
13
|
+
"preToolHook",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const REQUIRED_METHODS = Object.freeze([
|
|
17
|
+
"isAvailable",
|
|
18
|
+
"executable",
|
|
19
|
+
"defaultModel",
|
|
20
|
+
"tierModel",
|
|
21
|
+
"modelChoices",
|
|
22
|
+
"compatibilityStatus",
|
|
23
|
+
"buildMainInvocation",
|
|
24
|
+
"buildUtilityInvocation",
|
|
25
|
+
"createEventDecoder",
|
|
26
|
+
"normalizeEvent",
|
|
27
|
+
"authStatus",
|
|
28
|
+
"authHelp",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const CAPABILITY_SUPPORT = new Set(["native", "emulated", "unsupported"]);
|
|
32
|
+
const COMPATIBILITY_STATES = new Set(["compatible", "incompatible", "unknown", "not_checked"]);
|
|
33
|
+
const AUTH_STATES = new Set(["authenticated", "unauthenticated", "unknown", "not_checked"]);
|
|
34
|
+
|
|
35
|
+
class ProviderContractError extends Error {
|
|
36
|
+
constructor(message, details = {}) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = "ProviderContractError";
|
|
39
|
+
this.code = "INVALID_PROVIDER_CONTRACT";
|
|
40
|
+
this.details = details;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function own(object, key) {
|
|
45
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validateCapabilities(capabilities, providerId = "provider") {
|
|
49
|
+
if (!capabilities || typeof capabilities !== "object" || Array.isArray(capabilities)) {
|
|
50
|
+
throw new ProviderContractError(`${providerId}.capabilities must be an object`);
|
|
51
|
+
}
|
|
52
|
+
for (const key of CAPABILITY_KEYS) {
|
|
53
|
+
const capability = capabilities[key];
|
|
54
|
+
if (!capability || typeof capability !== "object" || Array.isArray(capability)) {
|
|
55
|
+
throw new ProviderContractError(`${providerId}.capabilities.${key} is required`);
|
|
56
|
+
}
|
|
57
|
+
if (!CAPABILITY_SUPPORT.has(capability.support)) {
|
|
58
|
+
throw new ProviderContractError(`${providerId}.capabilities.${key}.support must be native, emulated, or unsupported`);
|
|
59
|
+
}
|
|
60
|
+
if (!own(capability, "reason") || (capability.reason !== null && typeof capability.reason !== "string")) {
|
|
61
|
+
throw new ProviderContractError(`${providerId}.capabilities.${key}.reason must be an explicit string or null`);
|
|
62
|
+
}
|
|
63
|
+
if (capability.support !== "native" && !String(capability.reason || "").trim()) {
|
|
64
|
+
throw new ProviderContractError(`${providerId}.capabilities.${key}.reason must explain ${capability.support} support`);
|
|
65
|
+
}
|
|
66
|
+
if (key === "effort" && !Array.isArray(capability.values)) {
|
|
67
|
+
throw new ProviderContractError(`${providerId}.capabilities.effort.values must be an array`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return capabilities;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateProvider(provider) {
|
|
74
|
+
if (!provider || typeof provider !== "object" || Array.isArray(provider)) {
|
|
75
|
+
throw new ProviderContractError("provider must be an object");
|
|
76
|
+
}
|
|
77
|
+
if (typeof provider.id !== "string" || !provider.id.trim()) {
|
|
78
|
+
throw new ProviderContractError("provider.id must be a non-empty string");
|
|
79
|
+
}
|
|
80
|
+
if (typeof provider.label !== "string" || !provider.label.trim()) {
|
|
81
|
+
throw new ProviderContractError(`${provider.id}.label must be a non-empty string`);
|
|
82
|
+
}
|
|
83
|
+
for (const method of REQUIRED_METHODS) {
|
|
84
|
+
if (typeof provider[method] !== "function") {
|
|
85
|
+
throw new ProviderContractError(`${provider.id}.${method}() is required`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
validateCapabilities(provider.capabilities, provider.id);
|
|
89
|
+
return provider;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validateInvocation(invocation) {
|
|
93
|
+
if (!invocation || typeof invocation !== "object" || Array.isArray(invocation)) {
|
|
94
|
+
throw new ProviderContractError("provider invocation must be an object");
|
|
95
|
+
}
|
|
96
|
+
if (typeof invocation.binary !== "string" || !invocation.binary) {
|
|
97
|
+
throw new ProviderContractError("provider invocation binary must be a non-empty string");
|
|
98
|
+
}
|
|
99
|
+
if (!Array.isArray(invocation.args) || invocation.args.some((arg) => typeof arg !== "string")) {
|
|
100
|
+
throw new ProviderContractError("provider invocation args must be an array of strings");
|
|
101
|
+
}
|
|
102
|
+
if (!invocation.env || typeof invocation.env !== "object" || Array.isArray(invocation.env)) {
|
|
103
|
+
throw new ProviderContractError("provider invocation env must be an object");
|
|
104
|
+
}
|
|
105
|
+
if (Object.entries(invocation.env).some(([key, value]) => !key || typeof value !== "string")) {
|
|
106
|
+
throw new ProviderContractError("provider invocation env keys and values must be strings");
|
|
107
|
+
}
|
|
108
|
+
for (const key of ["stdin", "parserState"]) {
|
|
109
|
+
if (!own(invocation, key)) {
|
|
110
|
+
throw new ProviderContractError(`provider invocation ${key} is required`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (invocation.stdin !== null && typeof invocation.stdin !== "string" && !Buffer.isBuffer(invocation.stdin)) {
|
|
114
|
+
throw new ProviderContractError("provider invocation stdin must be null, a string, or a Buffer");
|
|
115
|
+
}
|
|
116
|
+
if (!invocation.parserState || typeof invocation.parserState !== "object" || Array.isArray(invocation.parserState)) {
|
|
117
|
+
throw new ProviderContractError("provider invocation parserState must be an object");
|
|
118
|
+
}
|
|
119
|
+
if (own(invocation, "warnings")) {
|
|
120
|
+
if (!Array.isArray(invocation.warnings) || invocation.warnings.some((warning) => (
|
|
121
|
+
!warning || typeof warning !== "object" || Array.isArray(warning)
|
|
122
|
+
|| typeof warning.code !== "string" || !warning.code.trim()
|
|
123
|
+
|| typeof warning.message !== "string" || !warning.message.trim()
|
|
124
|
+
))) {
|
|
125
|
+
throw new ProviderContractError("provider invocation warnings must contain code/message objects");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return invocation;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validateTypedStatus(value, kind) {
|
|
132
|
+
const states = kind === "compatibility" ? COMPATIBILITY_STATES : AUTH_STATES;
|
|
133
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || !states.has(value.state)) {
|
|
134
|
+
throw new ProviderContractError(`${kind} status must contain a typed state`);
|
|
135
|
+
}
|
|
136
|
+
if (own(value, "reason") && value.reason !== null && typeof value.reason !== "string") {
|
|
137
|
+
throw new ProviderContractError(`${kind} status reason must be a string or null`);
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
AUTH_STATES,
|
|
144
|
+
CAPABILITY_KEYS,
|
|
145
|
+
CAPABILITY_SUPPORT,
|
|
146
|
+
COMPATIBILITY_STATES,
|
|
147
|
+
ProviderContractError,
|
|
148
|
+
REQUIRED_METHODS,
|
|
149
|
+
validateCapabilities,
|
|
150
|
+
validateInvocation,
|
|
151
|
+
validateProvider,
|
|
152
|
+
validateTypedStatus,
|
|
153
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Security boundary: these builders isolate inherited environment variables
|
|
4
|
+
// between native provider children. They do not sandbox the process. A native
|
|
5
|
+
// same-UID child can still read files its OS user can read, including provider
|
|
6
|
+
// auth stores. This is environment isolation, not filesystem isolation or a
|
|
7
|
+
// containment claim.
|
|
8
|
+
const NATIVE_ISOLATION_NOTICE = "Native provider execution uses environment isolation only; it is not filesystem isolation, and same-UID processes retain the operating system user's file access.";
|
|
9
|
+
|
|
10
|
+
const STANDARD_PROCESS_ENV_KEYS = Object.freeze([
|
|
11
|
+
"PATH",
|
|
12
|
+
"HOME",
|
|
13
|
+
"TMPDIR",
|
|
14
|
+
"TMP",
|
|
15
|
+
"TEMP",
|
|
16
|
+
"USER",
|
|
17
|
+
"LOGNAME",
|
|
18
|
+
"SHELL",
|
|
19
|
+
"LANG",
|
|
20
|
+
"LANGUAGE",
|
|
21
|
+
"LC_ALL",
|
|
22
|
+
"LC_CTYPE",
|
|
23
|
+
"TZ",
|
|
24
|
+
"TERM",
|
|
25
|
+
"COLORTERM",
|
|
26
|
+
"NO_COLOR",
|
|
27
|
+
"FORCE_COLOR",
|
|
28
|
+
// The hook subprocess is another Open Claudia entry point. Keep its state
|
|
29
|
+
// rooted with the parent without exposing either provider's auth store.
|
|
30
|
+
"OPEN_CLAUDIA_CONFIG_DIR",
|
|
31
|
+
"SystemRoot",
|
|
32
|
+
"ComSpec",
|
|
33
|
+
"PATHEXT",
|
|
34
|
+
"WINDIR",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const PROVIDER_CREDENTIAL_KEYS = Object.freeze([
|
|
38
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
39
|
+
"ANTHROPIC_API_KEY",
|
|
40
|
+
"OPENAI_API_KEY",
|
|
41
|
+
"CURSOR_API_KEY",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const PROVIDER_CONFIG_ROOT_KEYS = Object.freeze([
|
|
45
|
+
"CLAUDE_CONFIG_DIR",
|
|
46
|
+
"CODEX_HOME",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const CONTROL_PLANE_KEYS = Object.freeze([
|
|
50
|
+
"TELEGRAM_BOT_TOKEN",
|
|
51
|
+
"KAZEE_BOT_TOKEN",
|
|
52
|
+
"BOT_CONTROL_TOKEN",
|
|
53
|
+
"OC_SEND_TOKEN",
|
|
54
|
+
"VOICE_BRIDGE_TOKEN",
|
|
55
|
+
"ELEVENLABS_API_KEY",
|
|
56
|
+
"AGENTSPACE_POD_TOKEN",
|
|
57
|
+
"WEB_PASSWORD",
|
|
58
|
+
"OPEN_CLAUDIA_KEYRING_CANARY",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const ALWAYS_STRIPPED_KEYS = new Set([
|
|
62
|
+
...PROVIDER_CREDENTIAL_KEYS,
|
|
63
|
+
...PROVIDER_CONFIG_ROOT_KEYS,
|
|
64
|
+
...CONTROL_PLANE_KEYS,
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
function copyEnvValue(target, source, key) {
|
|
68
|
+
const value = source[key];
|
|
69
|
+
if (value === undefined || value === null) return;
|
|
70
|
+
target[key] = String(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function pickStandardProcessEnv(source = process.env) {
|
|
74
|
+
const env = {};
|
|
75
|
+
for (const key of STANDARD_PROCESS_ENV_KEYS) copyEnvValue(env, source, key);
|
|
76
|
+
return env;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function passthroughKeys(source = process.env, configured = source.AGENT_ENV_PASSTHROUGH) {
|
|
80
|
+
const keys = String(configured || "")
|
|
81
|
+
.split(",")
|
|
82
|
+
.map((key) => key.trim())
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
for (const key of keys) {
|
|
85
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || ["__proto__", "prototype", "constructor"].includes(key)) {
|
|
86
|
+
const error = new Error(`Invalid AGENT_ENV_PASSTHROUGH key: ${key}`);
|
|
87
|
+
error.code = "INVALID_ENV_PASSTHROUGH";
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return [...new Set(keys)];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function addConfiguredPassthrough(target, source = process.env, configured = source.AGENT_ENV_PASSTHROUGH) {
|
|
95
|
+
for (const key of passthroughKeys(source, configured)) copyEnvValue(target, source, key);
|
|
96
|
+
return target;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function baseAgentEnv(source = process.env, options = {}) {
|
|
100
|
+
const env = pickStandardProcessEnv(source);
|
|
101
|
+
addConfiguredPassthrough(env, source, options.passthrough);
|
|
102
|
+
// Strip after passthrough so a broad/operator-supplied list cannot re-open a
|
|
103
|
+
// provider credential, control-plane token, or keyring canary boundary.
|
|
104
|
+
for (const key of ALWAYS_STRIPPED_KEYS) delete env[key];
|
|
105
|
+
return env;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function providerValue(source, overrides, key) {
|
|
109
|
+
if (Object.prototype.hasOwnProperty.call(overrides, key)) return overrides[key];
|
|
110
|
+
return source[key];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function copyProviderValue(env, source, overrides, key) {
|
|
114
|
+
const value = providerValue(source, overrides, key);
|
|
115
|
+
if (value === undefined || value === null || String(value) === "") return;
|
|
116
|
+
env[key] = String(value);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function claudeAgentEnv(source = process.env, overrides = {}) {
|
|
120
|
+
const env = baseAgentEnv(source, overrides);
|
|
121
|
+
for (const key of ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "CLAUDE_CONFIG_DIR"]) {
|
|
122
|
+
copyProviderValue(env, source, overrides, key);
|
|
123
|
+
}
|
|
124
|
+
return env;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function codexAgentEnv(source = process.env, overrides = {}) {
|
|
128
|
+
const env = baseAgentEnv(source, overrides);
|
|
129
|
+
for (const key of ["OPENAI_API_KEY", "CODEX_HOME"]) {
|
|
130
|
+
copyProviderValue(env, source, overrides, key);
|
|
131
|
+
}
|
|
132
|
+
return env;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
ALWAYS_STRIPPED_KEYS,
|
|
137
|
+
CONTROL_PLANE_KEYS,
|
|
138
|
+
NATIVE_ISOLATION_NOTICE,
|
|
139
|
+
PROVIDER_CONFIG_ROOT_KEYS,
|
|
140
|
+
PROVIDER_CREDENTIAL_KEYS,
|
|
141
|
+
STANDARD_PROCESS_ENV_KEYS,
|
|
142
|
+
addConfiguredPassthrough,
|
|
143
|
+
baseAgentEnv,
|
|
144
|
+
claudeAgentEnv,
|
|
145
|
+
codexAgentEnv,
|
|
146
|
+
passthroughKeys,
|
|
147
|
+
pickStandardProcessEnv,
|
|
148
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const NORMALIZED_EVENT_TYPES = Object.freeze([
|
|
4
|
+
"session",
|
|
5
|
+
"text_delta",
|
|
6
|
+
"text_final",
|
|
7
|
+
"tool_start",
|
|
8
|
+
"tool_end",
|
|
9
|
+
"usage",
|
|
10
|
+
"result",
|
|
11
|
+
"error",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const DEFAULT_MAX_LINE_BYTES = 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
function decoderError(code, message, details = {}) {
|
|
17
|
+
return { type: "decoder.error", code, message, ...details };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseCompleteLine(line) {
|
|
21
|
+
let end = line.length;
|
|
22
|
+
if (end > 0 && line[end - 1] === 0x0d) end -= 1;
|
|
23
|
+
const text = line.subarray(0, end).toString("utf8").trim();
|
|
24
|
+
if (!text) return [];
|
|
25
|
+
try {
|
|
26
|
+
return [JSON.parse(text)];
|
|
27
|
+
} catch (error) {
|
|
28
|
+
// Do not include the source line or parser detail: provider output can
|
|
29
|
+
// contain prompts, tool results, paths, or credentials.
|
|
30
|
+
return [decoderError("MALFORMED_JSON", "Malformed provider JSONL record")];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createJsonlDecoder(options = {}) {
|
|
35
|
+
const maxLineBytes = options.maxLineBytes ?? DEFAULT_MAX_LINE_BYTES;
|
|
36
|
+
if (!Number.isSafeInteger(maxLineBytes) || maxLineBytes < 1) {
|
|
37
|
+
throw new TypeError("maxLineBytes must be a positive safe integer");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let tail = Buffer.alloc(0);
|
|
41
|
+
let discarding = false;
|
|
42
|
+
let closed = false;
|
|
43
|
+
|
|
44
|
+
const api = {
|
|
45
|
+
push(input) {
|
|
46
|
+
if (closed) {
|
|
47
|
+
const error = new Error("provider JSONL decoder is closed");
|
|
48
|
+
error.code = "DECODER_CLOSED";
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
if (input === undefined || input === null) return [];
|
|
52
|
+
const chunk = Buffer.isBuffer(input)
|
|
53
|
+
? input
|
|
54
|
+
: (input instanceof Uint8Array ? Buffer.from(input.buffer, input.byteOffset, input.byteLength) : Buffer.from(String(input), "utf8"));
|
|
55
|
+
const events = [];
|
|
56
|
+
let offset = 0;
|
|
57
|
+
|
|
58
|
+
while (offset < chunk.length) {
|
|
59
|
+
if (discarding) {
|
|
60
|
+
const newline = chunk.indexOf(0x0a, offset);
|
|
61
|
+
if (newline === -1) return events;
|
|
62
|
+
discarding = false;
|
|
63
|
+
offset = newline + 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const newline = chunk.indexOf(0x0a, offset);
|
|
68
|
+
if (newline === -1) {
|
|
69
|
+
const remaining = chunk.subarray(offset);
|
|
70
|
+
if (tail.length + remaining.length > maxLineBytes) {
|
|
71
|
+
tail = Buffer.alloc(0);
|
|
72
|
+
discarding = true;
|
|
73
|
+
events.push(decoderError(
|
|
74
|
+
"LINE_TOO_LARGE",
|
|
75
|
+
`Provider JSONL record exceeded ${maxLineBytes} bytes`,
|
|
76
|
+
{ maxLineBytes },
|
|
77
|
+
));
|
|
78
|
+
} else if (remaining.length > 0) {
|
|
79
|
+
tail = tail.length ? Buffer.concat([tail, remaining]) : Buffer.from(remaining);
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const segment = chunk.subarray(offset, newline);
|
|
85
|
+
const lineBytes = tail.length + segment.length;
|
|
86
|
+
if (lineBytes > maxLineBytes) {
|
|
87
|
+
events.push(decoderError(
|
|
88
|
+
"LINE_TOO_LARGE",
|
|
89
|
+
`Provider JSONL record exceeded ${maxLineBytes} bytes`,
|
|
90
|
+
{ maxLineBytes },
|
|
91
|
+
));
|
|
92
|
+
} else {
|
|
93
|
+
const line = tail.length ? Buffer.concat([tail, segment]) : segment;
|
|
94
|
+
events.push(...parseCompleteLine(line));
|
|
95
|
+
}
|
|
96
|
+
tail = Buffer.alloc(0);
|
|
97
|
+
offset = newline + 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return events;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
end() {
|
|
104
|
+
if (closed) return [];
|
|
105
|
+
closed = true;
|
|
106
|
+
if (discarding) {
|
|
107
|
+
tail = Buffer.alloc(0);
|
|
108
|
+
discarding = false;
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
const finalLine = tail;
|
|
112
|
+
tail = Buffer.alloc(0);
|
|
113
|
+
return finalLine.length ? parseCompleteLine(finalLine) : [];
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
get bufferedBytes() { return tail.length; },
|
|
117
|
+
get discarding() { return discarding; },
|
|
118
|
+
get closed() { return closed; },
|
|
119
|
+
maxLineBytes,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return api;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function providerErrorFlags(message) {
|
|
126
|
+
const text = String(message || "").toLowerCase();
|
|
127
|
+
return {
|
|
128
|
+
authError: /unauthori[sz]ed|not (?:logged in|authenticated)|unauthenticated|login required|authentication failed|auth failed|invalid api key|no credentials|401/.test(text),
|
|
129
|
+
usageLimit: /usage limit|spend limit|monthly cycle|quota|rate limit/.test(text),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeDecoderError(rawEvent) {
|
|
134
|
+
if (!rawEvent || rawEvent.type !== "decoder.error") return null;
|
|
135
|
+
return {
|
|
136
|
+
type: "error",
|
|
137
|
+
message: rawEvent.message || "Provider stream decoding failed",
|
|
138
|
+
authError: false,
|
|
139
|
+
usageLimit: false,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function usageFingerprint(usage, scope) {
|
|
144
|
+
if (!usage || typeof usage !== "object") return null;
|
|
145
|
+
const ordered = {};
|
|
146
|
+
for (const key of Object.keys(usage).sort()) ordered[key] = usage[key];
|
|
147
|
+
return `${scope}:${JSON.stringify(ordered)}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function normalizedUsage(usage, scope, seenUsage) {
|
|
151
|
+
const fingerprint = usageFingerprint(usage, scope);
|
|
152
|
+
if (!fingerprint || seenUsage.has(fingerprint)) return null;
|
|
153
|
+
seenUsage.add(fingerprint);
|
|
154
|
+
return { type: "usage", usage, scope };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function selectTerminalText({ resultText = "", finalText = "", deltaText = "" } = {}) {
|
|
158
|
+
return resultText || finalText || deltaText;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
DEFAULT_MAX_LINE_BYTES,
|
|
163
|
+
NORMALIZED_EVENT_TYPES,
|
|
164
|
+
createJsonlDecoder,
|
|
165
|
+
normalizeDecoderError,
|
|
166
|
+
normalizedUsage,
|
|
167
|
+
providerErrorFlags,
|
|
168
|
+
selectTerminalText,
|
|
169
|
+
usageFingerprint,
|
|
170
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
function denyGateCliPath() {
|
|
6
|
+
return path.resolve(__dirname, "..", "..", "bin", "cli.js");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function shellQuote(value) {
|
|
10
|
+
return `'${String(value).replace(/'/g, `'"'"'`)}'`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function buildDenyGateCommand({ nodePath = process.execPath, cliPath = denyGateCliPath() } = {}) {
|
|
14
|
+
if (!path.isAbsolute(nodePath) || !path.isAbsolute(cliPath)) {
|
|
15
|
+
const error = new Error("Tool hook executables must use absolute paths");
|
|
16
|
+
error.code = "INVALID_TOOL_HOOK_COMMAND";
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
return `${shellQuote(nodePath)} ${shellQuote(cliPath)} deny-gate-hook`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { buildDenyGateCommand, denyGateCliPath, shellQuote };
|