@inneranimalmedia/agentsam-sdk 2.4.1 → 2.6.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.
Files changed (233) hide show
  1. package/AGENTSAM.md +55 -0
  2. package/README.md +17 -12
  3. package/bin/agentsam +2 -0
  4. package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
  5. package/docs/AGENTSAM_WORKBENCH.md +30 -0
  6. package/docs/AUTH_IDENTITY_CONTRACT.md +52 -0
  7. package/docs/CAPABILITIES.md +5 -3
  8. package/docs/CLI_SHELL.md +167 -80
  9. package/docs/CMS_STUDIO.md +101 -0
  10. package/docs/CONTEXT.md +170 -0
  11. package/docs/LOCAL_OLLAMA.md +58 -0
  12. package/docs/MERKLE.md +19 -19
  13. package/docs/PORTABLE_CONTEXT.md +4 -3
  14. package/docs/PROJECT_CONFIG.md +72 -0
  15. package/docs/RELEASES.md +35 -2
  16. package/docs/REPOSITORY_INTELLIGENCE.md +1 -1
  17. package/docs/REPOSITORY_KNOWLEDGE.md +114 -0
  18. package/docs/SDK_WORKER.md +86 -0
  19. package/docs/SECURITY.md +59 -22
  20. package/docs/client-cms-editor.md +15 -0
  21. package/docs/local-studio/WORKMODE_DONOR_NOTES.md +485 -0
  22. package/package.json +35 -10
  23. package/packages/connectors/cloudflare/package.json +10 -0
  24. package/packages/connectors/cloudflare/src/index.js +127 -0
  25. package/packages/connectors/cloudflare/src/owner.js +76 -0
  26. package/packages/connectors/cloudflare/src/routes.js +223 -0
  27. package/packages/connectors/cloudflare/src/vault.js +80 -0
  28. package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
  29. package/packages/identity/package.json +10 -1
  30. package/packages/identity/src/contracts/auth-config.js +109 -0
  31. package/packages/identity/src/index.js +1 -0
  32. package/packages/identity/src/oauth/README.md +2 -2
  33. package/packages/identity/src/oauth/credentials.js +11 -4
  34. package/packages/identity/src/oauth/iam-platform.js +3 -3
  35. package/packages/identity/src/providers/iam/index.js +7 -7
  36. package/packages/identity/src/providers/iam/oauth.js +6 -4
  37. package/packages/identity/src/providers/iam/profile.js +5 -5
  38. package/packages/identity/tests/auth-config.test.mjs +61 -0
  39. package/packages/identity/tests/oauth-credentials.test.mjs +13 -2
  40. package/protocol/FILEMETA_V1.md +95 -0
  41. package/protocol/INSPECT_VIEWS_V1.md +40 -0
  42. package/protocol/MERKLE_PERSISTENCE_V1.md +50 -0
  43. package/protocol/MERKLE_V1.md +3 -1
  44. package/protocol/README.md +1 -0
  45. package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
  46. package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
  47. package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
  48. package/protocol/capabilities/manifest.json +79 -3
  49. package/protocol/capabilities/repository-snapshot.schema.json +1 -0
  50. package/protocol/context/context-budget.schema.json +36 -0
  51. package/protocol/context/context-item.schema.json +19 -0
  52. package/protocol/context/resolved-context-pack.schema.json +47 -0
  53. package/protocol/context/result-policy.schema.json +17 -0
  54. package/protocol/knowledge/context-pack.schema.json +33 -0
  55. package/protocol/knowledge/retrieval-query.schema.json +68 -13
  56. package/protocol/models/README.md +373 -0
  57. package/protocol/models/model-inventory-v2.schema.json +212 -0
  58. package/python/README.md +15 -7
  59. package/python/agentsam_sdk/cli.py +0 -21
  60. package/python/agentsam_sdk/tui/README.md +17 -12
  61. package/python/agentsam_sdk/tui/bootstrap.py +2 -2
  62. package/python/agentsam_sdk/tui/demo.py +25 -10
  63. package/python/agentsam_sdk/tui/onboarding.py +208 -0
  64. package/python/tests/test_tui_cli.py +7 -6
  65. package/skills/README.md +21 -0
  66. package/skills/agentsam-app-fundamentals/SKILL.md +165 -0
  67. package/skills/agentsam-app-fundamentals/references/graphs-contracts-ast-merkle.md +89 -0
  68. package/skills/agentsam-app-fundamentals/references/trust-credentials-and-destinations.md +99 -0
  69. package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
  70. package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
  71. package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
  72. package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
  73. package/skills/agentsam-jr-dev/SKILL.md +232 -0
  74. package/skills/agentsam-jr-dev/references/real-application-logic.md +156 -0
  75. package/skills/agentsam-jr-dev/references/web-application-fundamentals.md +240 -0
  76. package/skills/agentsam-progression-guard/SKILL.md +197 -0
  77. package/skills/agentsam-progression-guard/references/checkpoint-chain.md +111 -0
  78. package/skills/agentsam-progression-guard/references/hooks-operational-io.md +96 -0
  79. package/skills/catalog.json +71 -0
  80. package/src/agent/capability-adapter.js +25 -13
  81. package/src/agent/index.js +1 -0
  82. package/src/agent/responses-runner.js +325 -0
  83. package/src/capabilities/index.js +7 -0
  84. package/src/capabilities/repository-snapshot-view.js +238 -0
  85. package/src/capabilities/repository-snapshot.js +28 -14
  86. package/src/cli.js +198 -76
  87. package/src/cloudflare/cpu-profile.js +115 -0
  88. package/src/cloudflare/index.js +14 -0
  89. package/src/cloudflare/wrangler.js +132 -0
  90. package/src/commands/account-auth.js +47 -0
  91. package/src/commands/cloudflare.js +58 -0
  92. package/src/commands/connections.js +93 -0
  93. package/src/commands/context-economics.js +114 -0
  94. package/src/commands/context.js +14 -2
  95. package/src/commands/db.js +4 -7
  96. package/src/commands/deploy.js +54 -32
  97. package/src/commands/eval.js +63 -0
  98. package/src/commands/interactive.js +18 -0
  99. package/src/commands/knowledge.js +27 -4
  100. package/src/commands/merkle-persist.js +118 -0
  101. package/src/commands/merkle.js +32 -17
  102. package/src/commands/models.js +152 -0
  103. package/src/commands/ollama.js +259 -0
  104. package/src/commands/preferences.js +144 -0
  105. package/src/commands/product.js +86 -16
  106. package/src/commands/resume.js +67 -0
  107. package/src/commands/security.js +8 -6
  108. package/src/commands/shell.js +405 -100
  109. package/src/commands/skills.js +66 -0
  110. package/src/commands/start-local.js +1 -1
  111. package/src/commands/tunnel.js +5 -4
  112. package/src/commands/whoami.js +86 -0
  113. package/src/context/budget.js +120 -0
  114. package/src/context/compact.js +28 -0
  115. package/src/context/index.js +7 -0
  116. package/src/context/rehydrate.js +35 -0
  117. package/src/context/resolve.js +116 -0
  118. package/src/context/result-policy.js +66 -0
  119. package/src/errors/diagnostic.js +160 -0
  120. package/src/errors/index.js +9 -0
  121. package/src/eval/context.js +191 -0
  122. package/src/eval/index.js +1 -0
  123. package/src/index.js +70 -0
  124. package/src/indexing/execution-boundary.js +144 -0
  125. package/src/indexing/index.js +8 -0
  126. package/src/indexing/provider.js +41 -0
  127. package/src/knowledge/config.js +2 -2
  128. package/src/knowledge/context-pack.js +12 -1
  129. package/src/knowledge/contracts.js +9 -4
  130. package/src/knowledge/engine.js +7 -3
  131. package/src/knowledge/service/server.js +1 -1
  132. package/src/lib/account-session.js +98 -0
  133. package/src/lib/agent-instructions.js +73 -0
  134. package/src/lib/auth.js +4 -0
  135. package/src/lib/bridge-client.js +7 -5
  136. package/src/lib/cli-preferences.js +78 -0
  137. package/src/lib/core-client.js +8 -8
  138. package/src/lib/deploy/git-guard.js +69 -0
  139. package/src/lib/deploy/health.js +57 -0
  140. package/src/lib/deploy/local-studio.js +283 -0
  141. package/src/lib/deploy/secret-scan.js +65 -0
  142. package/src/lib/deploy-receipt/index.js +5 -2
  143. package/src/lib/detect-context.js +6 -5
  144. package/src/lib/execution-approvals.js +59 -0
  145. package/src/lib/identity-scaffold.js +1 -1
  146. package/src/lib/local-scaffold.js +35 -33
  147. package/src/lib/local-sessions.js +127 -0
  148. package/src/lib/local-status.js +9 -17
  149. package/src/lib/merkle/cloudflare-persistence.js +321 -0
  150. package/src/lib/merkle/filemeta.js +43 -0
  151. package/src/lib/merkle/git-ignore.js +24 -0
  152. package/src/lib/merkle/hash.js +1 -0
  153. package/src/lib/merkle/index.js +22 -0
  154. package/src/lib/merkle/persistence.js +72 -0
  155. package/src/lib/merkle/semantic.js +359 -0
  156. package/src/lib/merkle/snapshot.js +9 -3
  157. package/src/lib/merkle/tree.js +11 -6
  158. package/src/lib/project-config.js +227 -0
  159. package/src/lib/project-rules.js +68 -0
  160. package/src/lib/provider-credentials.js +83 -0
  161. package/src/lib/save-sdk-token.js +1 -1
  162. package/src/lib/scaffold/templates/worker-api/index.js +101 -20
  163. package/src/lib/scaffold/wizards/worker-api.js +27 -11
  164. package/src/lib/slash-commands.js +22 -15
  165. package/src/lib/tools.js +11 -5
  166. package/src/models/catalog.js +135 -0
  167. package/src/models/index.js +7 -0
  168. package/src/providers/index.js +5 -0
  169. package/src/providers/openai-responses.js +275 -0
  170. package/src/security/index.js +1 -0
  171. package/src/security/inventory.js +4 -1
  172. package/src/security/process.js +35 -9
  173. package/src/security/render.js +27 -5
  174. package/src/security/scan.js +24 -9
  175. package/src/security/trust-boundary.js +24 -0
  176. package/src/skills/index.js +64 -0
  177. package/src/telemetry/contracts.js +203 -0
  178. package/src/telemetry/events.js +48 -0
  179. package/src/telemetry/index.js +8 -0
  180. package/src/tools/hydrate.js +35 -0
  181. package/src/tools/index.js +2 -0
  182. package/src/tools/search.js +70 -0
  183. package/src/ui/ansi.js +1 -1
  184. package/src/ui/boot.js +54 -0
  185. package/src/ui/merkle/render.js +1 -0
  186. package/src/ui/runtime-activity.js +192 -0
  187. package/src/ui/theme.js +19 -18
  188. package/test/account-session.test.mjs +36 -0
  189. package/test/app-building-skills.test.mjs +61 -0
  190. package/test/apps-scaffold-contract.test.mjs +56 -0
  191. package/test/capabilities.test.mjs +53 -4
  192. package/test/cli-preferences.test.mjs +46 -0
  193. package/test/cloudflare-connector.test.mjs +96 -0
  194. package/test/cloudflare-runtime.test.mjs +75 -0
  195. package/test/context.test.mjs +144 -0
  196. package/test/deploy-health-scan.test.mjs +67 -0
  197. package/test/error-diagnostics.test.mjs +59 -0
  198. package/test/eval-context.test.mjs +37 -0
  199. package/test/execution-approvals.test.mjs +27 -0
  200. package/test/indexing-provider.test.mjs +29 -0
  201. package/test/jr-dev-skill.test.mjs +26 -0
  202. package/test/knowledge-context-pack.test.mjs +19 -0
  203. package/test/knowledge.test.mjs +1 -1
  204. package/test/local-sessions.test.mjs +42 -0
  205. package/test/local-studio-deploy.test.mjs +83 -0
  206. package/test/merkle-persistence.test.mjs +91 -0
  207. package/test/merkle.test.mjs +77 -3
  208. package/test/model-catalog.test.mjs +43 -0
  209. package/test/models.test.mjs +51 -0
  210. package/test/npm10-lock.test.mjs +29 -0
  211. package/test/ollama.test.mjs +94 -0
  212. package/test/openai-responses.test.mjs +95 -0
  213. package/test/project-config.test.mjs +81 -0
  214. package/test/project-rules.test.mjs +44 -0
  215. package/test/provider-credentials.test.mjs +52 -0
  216. package/test/rehydrate.test.mjs +25 -0
  217. package/test/release-hygiene.test.mjs +34 -0
  218. package/test/repository-snapshot-view.test.mjs +112 -0
  219. package/test/responses-runner.test.mjs +148 -0
  220. package/test/runtime-activity.test.mjs +98 -0
  221. package/test/sdk-worker-contract.test.mjs +68 -0
  222. package/test/security.test.mjs +46 -0
  223. package/test/shell.test.mjs +55 -21
  224. package/test/skills.test.mjs +22 -0
  225. package/test/smoke.mjs +5 -2
  226. package/test/telemetry.test.mjs +79 -0
  227. package/test/theme-portability.test.mjs +14 -0
  228. package/test/tools-search.test.mjs +40 -0
  229. package/test/whoami-resume.test.mjs +56 -0
  230. package/examples/agentsam-tui-ansi.mjs +0 -149
  231. package/src/commands/tui.js +0 -120
  232. package/src/ui/splash-xterm.js +0 -290
  233. package/src/ui/splash.js +0 -426
@@ -0,0 +1,275 @@
1
+ import { calculateModelCost, getModelRecord } from '../models/index.js';
2
+ import { createAgentEvent, createUsageSnapshot } from '../telemetry/index.js';
3
+ import { createOpenAIHttpError, diagnosticFromError } from '../errors/index.js';
4
+
5
+ const DEFAULT_BASE_URL = 'https://api.openai.com/v1';
6
+
7
+ function clean(value) { return value == null ? '' : String(value).trim(); }
8
+ function integer(value) { const n = Number(value ?? 0); return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0; }
9
+
10
+ function normalizeServiceTier(value, requested = 'default') {
11
+ const tier = clean(value).toLowerCase();
12
+ if (tier === 'priority') return 'fast';
13
+ if (tier === 'auto' || !tier) return requested === 'auto' ? 'default' : requested;
14
+ return tier;
15
+ }
16
+
17
+ export function extractOpenAIOutputText(response = {}) {
18
+ if (typeof response.output_text === 'string') return response.output_text;
19
+ const chunks = [];
20
+ for (const item of response.output || []) {
21
+ if (item?.type !== 'message') continue;
22
+ for (const content of item.content || []) {
23
+ if (content?.type === 'output_text' && typeof content.text === 'string') chunks.push(content.text);
24
+ }
25
+ }
26
+ return chunks.join('');
27
+ }
28
+
29
+ export function extractOpenAIFunctionCalls(response = {}) {
30
+ return (response.output || []).filter((item) => item?.type === 'function_call').map((item) => Object.freeze({
31
+ id: item.id || null,
32
+ call_id: item.call_id,
33
+ name: item.name,
34
+ arguments: item.arguments || '{}',
35
+ status: item.status || null,
36
+ }));
37
+ }
38
+
39
+ function usageParts(response = {}) {
40
+ const usage = response.usage || {};
41
+ return {
42
+ input_tokens: integer(usage.input_tokens),
43
+ cached_input_tokens: integer(usage.input_tokens_details?.cached_tokens),
44
+ cache_write_tokens: integer(usage.input_tokens_details?.cache_write_tokens),
45
+ output_tokens: integer(usage.output_tokens),
46
+ reasoning_tokens: integer(usage.output_tokens_details?.reasoning_tokens),
47
+ };
48
+ }
49
+
50
+ function errorMessage(body, status) {
51
+ return body?.error?.message || body?.message || `OpenAI Responses API returned HTTP ${status}`;
52
+ }
53
+
54
+ function optionalSignal(timeoutMs) {
55
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return undefined;
56
+ if (typeof AbortSignal?.timeout !== 'function') return undefined;
57
+ return AbortSignal.timeout(Math.floor(timeoutMs));
58
+ }
59
+
60
+ function normalizeTools(tools = []) {
61
+ if (!Array.isArray(tools)) throw new TypeError('tools must be an array');
62
+ return tools.map((tool) => {
63
+ if (tool?.type !== 'function' || !clean(tool.name)) throw new TypeError('OpenAI adapter tools must be Responses function tool descriptors');
64
+ return {
65
+ type: 'function',
66
+ name: clean(tool.name),
67
+ description: clean(tool.description) || undefined,
68
+ parameters: tool.parameters || { type: 'object', properties: {} },
69
+ strict: tool.strict !== false,
70
+ ...(tool.async === true ? { async: true } : {}),
71
+ };
72
+ });
73
+ }
74
+
75
+ function assertRuntimeConfig(model, reasoningEffort, serviceTier) {
76
+ const record = getModelRecord(model);
77
+ if (!record || record.provider !== 'openai') throw new RangeError(`unsupported OpenAI model catalog entry: ${model}`);
78
+ if (!record.reasoning_efforts.includes(reasoningEffort)) throw new RangeError(`unsupported reasoning effort for ${record.provider_model_id}: ${reasoningEffort}`);
79
+ if (!record.service_tiers.includes(serviceTier)) throw new RangeError(`unsupported service tier for ${record.provider_model_id}: ${serviceTier}`);
80
+ return record;
81
+ }
82
+
83
+ function emitEvent(emit, type, payload, meta = {}) {
84
+ if (typeof emit !== 'function') return;
85
+ emit(createAgentEvent(type, payload, meta));
86
+ }
87
+
88
+ export function createOpenAIResponsesAdapter(options = {}) {
89
+ const apiKey = clean(options.apiKey || process.env.OPENAI_API_KEY);
90
+ const baseUrl = clean(options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
91
+ const fetchImpl = options.fetchImpl || fetch;
92
+ const defaultEmit = options.emit;
93
+ const timeoutMs = options.timeoutMs;
94
+
95
+ async function request(pathname, body, runtime = {}) {
96
+ if (!apiKey) throw new Error('OPENAI_API_KEY is required for OpenAI Responses execution');
97
+ const response = await fetchImpl(`${baseUrl}${pathname}`, {
98
+ method: 'POST',
99
+ headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
100
+ body: JSON.stringify(body),
101
+ signal: optionalSignal(runtime.timeoutMs ?? timeoutMs),
102
+ });
103
+ let rawText = '';
104
+ let parsed = null;
105
+ if (typeof response.text === 'function') {
106
+ try { rawText = await response.text(); } catch { rawText = ''; }
107
+ if (rawText) {
108
+ try { parsed = JSON.parse(rawText); } catch { parsed = null; }
109
+ }
110
+ } else if (typeof response.json === 'function') {
111
+ try {
112
+ parsed = await response.json();
113
+ rawText = JSON.stringify(parsed ?? null);
114
+ } catch {
115
+ parsed = null;
116
+ rawText = '';
117
+ }
118
+ }
119
+ if (!response.ok) {
120
+ throw createOpenAIHttpError({
121
+ status: response.status,
122
+ body: parsed,
123
+ rawText,
124
+ headers: response.headers,
125
+ requestedServiceTier: body?.service_tier,
126
+ });
127
+ }
128
+ return Object.freeze({
129
+ data: parsed ?? {},
130
+ http: Object.freeze({
131
+ status: response.status,
132
+ request_id: clean(response.headers?.get?.('x-request-id') || response.headers?.get?.('openai-request-id')) || null,
133
+ ray_id: clean(response.headers?.get?.('cf-ray')) || null,
134
+ }),
135
+ });
136
+ }
137
+
138
+ async function create(params = {}) {
139
+ const modelRecord = getModelRecord(params.model);
140
+ const model = modelRecord?.provider_model_id || clean(params.model);
141
+ const reasoningEffort = clean(params.reasoningEffort || params.reasoning_effort || 'low');
142
+ const serviceTier = clean(params.serviceTier || params.service_tier || 'default');
143
+ const record = assertRuntimeConfig(model, reasoningEffort, serviceTier);
144
+ const emit = params.emit || defaultEmit;
145
+ const meta = { runId: params.runId, sequence: params.sequence };
146
+ const tools = normalizeTools(params.tools || []);
147
+ const body = {
148
+ model,
149
+ input: params.input ?? '',
150
+ reasoning: { effort: reasoningEffort },
151
+ service_tier: serviceTier,
152
+ store: params.store !== false,
153
+ truncation: 'disabled',
154
+ parallel_tool_calls: params.parallelToolCalls !== false,
155
+ ...(clean(params.instructions) ? { instructions: String(params.instructions) } : {}),
156
+ ...(tools.length ? { tools } : {}),
157
+ ...(clean(params.previousResponseId) ? { previous_response_id: clean(params.previousResponseId) } : {}),
158
+ ...(Number.isInteger(params.maxOutputTokens) && params.maxOutputTokens > 0 ? { max_output_tokens: params.maxOutputTokens } : {}),
159
+ ...(clean(params.promptCacheKey) ? { prompt_cache_key: clean(params.promptCacheKey) } : {}),
160
+ ...(params.promptCacheOptions ? { prompt_cache_options: params.promptCacheOptions } : {}),
161
+ ...(params.metadata ? { metadata: params.metadata } : {}),
162
+ ...(params.background === true ? { background: true } : {}),
163
+ };
164
+
165
+ emitEvent(emit, 'model.started', {
166
+ provider: 'openai', model, reasoning_effort: reasoningEffort, requested_service_tier: serviceTier,
167
+ }, meta);
168
+
169
+ let response;
170
+ let http;
171
+ try {
172
+ const result = await request('/responses', body, params);
173
+ response = result.data;
174
+ http = result.http;
175
+ } catch (error) {
176
+ const diagnostic = diagnosticFromError(error, { source: 'openai', kind: 'provider_error' });
177
+ emitEvent(emit, 'error.observed', diagnostic, meta);
178
+ emitEvent(emit, 'run.failed', { stage: 'model', provider: 'openai', model, error: diagnostic }, meta);
179
+ throw error;
180
+ }
181
+
182
+ const delta = usageParts(response);
183
+ const actualServiceTier = normalizeServiceTier(response.service_tier, serviceTier);
184
+ const cost = calculateModelCost(record, { ...delta, estimate_kind: 'provider' }, { serviceTier: actualServiceTier });
185
+ const usageSnapshot = createUsageSnapshot({
186
+ current_context: { input_tokens: delta.input_tokens, window_tokens: record.context_window },
187
+ cumulative: params.cumulativeUsage ? {
188
+ input_tokens: integer(params.cumulativeUsage.input_tokens) + delta.input_tokens,
189
+ output_tokens: integer(params.cumulativeUsage.output_tokens) + delta.output_tokens,
190
+ cached_input_tokens: integer(params.cumulativeUsage.cached_input_tokens) + delta.cached_input_tokens,
191
+ cache_write_tokens: integer(params.cumulativeUsage.cache_write_tokens) + delta.cache_write_tokens,
192
+ reasoning_tokens: integer(params.cumulativeUsage.reasoning_tokens) + delta.reasoning_tokens,
193
+ } : delta,
194
+ estimate_kind: 'provider',
195
+ });
196
+
197
+ emitEvent(emit, 'usage.snapshot', usageSnapshot, meta);
198
+ emitEvent(emit, 'cost.snapshot', cost, meta);
199
+ emitEvent(emit, 'model.completed', {
200
+ provider: 'openai', model, response_id: response.id, status: response.status,
201
+ request_id: http?.request_id || null, ray_id: http?.ray_id || null,
202
+ requested_service_tier: serviceTier, actual_service_tier: actualServiceTier,
203
+ }, meta);
204
+
205
+ return Object.freeze({
206
+ provider: 'openai',
207
+ model,
208
+ response_id: response.id,
209
+ status: response.status,
210
+ request_id: http?.request_id || null,
211
+ ray_id: http?.ray_id || null,
212
+ output_text: extractOpenAIOutputText(response),
213
+ tool_calls: Object.freeze(extractOpenAIFunctionCalls(response)),
214
+ usage_delta: Object.freeze(delta),
215
+ usage_snapshot: usageSnapshot,
216
+ cost,
217
+ requested_service_tier: serviceTier,
218
+ actual_service_tier: actualServiceTier,
219
+ raw: response,
220
+ });
221
+ }
222
+
223
+ async function continueWithToolOutputs(params = {}) {
224
+ const previousResponseId = clean(params.previousResponseId);
225
+ if (!previousResponseId) throw new TypeError('previousResponseId is required');
226
+ const outputs = (params.toolOutputs || []).map((row) => {
227
+ const callId = clean(row.call_id || row.callId);
228
+ if (!callId) throw new TypeError('tool output call_id is required');
229
+ const output = typeof row.output === 'string' ? row.output : JSON.stringify(row.output ?? null);
230
+ return { type: 'function_call_output', call_id: callId, output };
231
+ });
232
+ if (!outputs.length) throw new TypeError('at least one tool output is required');
233
+ return create({ ...params, previousResponseId, input: outputs });
234
+ }
235
+
236
+ async function compact(params = {}) {
237
+ const modelRecord = getModelRecord(params.model);
238
+ const model = modelRecord?.provider_model_id || clean(params.model);
239
+ if (!modelRecord || modelRecord.provider !== 'openai') throw new RangeError(`unsupported OpenAI model catalog entry: ${params.model}`);
240
+ const emit = params.emit || defaultEmit;
241
+ const meta = { runId: params.runId, sequence: params.sequence };
242
+ const body = {
243
+ model,
244
+ ...(clean(params.previousResponseId) ? { previous_response_id: clean(params.previousResponseId) } : {}),
245
+ ...(params.input != null ? { input: params.input } : {}),
246
+ ...(clean(params.instructions) ? { instructions: String(params.instructions) } : {}),
247
+ ...(clean(params.promptCacheKey) ? { prompt_cache_key: clean(params.promptCacheKey) } : {}),
248
+ ...(params.promptCacheOptions ? { prompt_cache_options: params.promptCacheOptions } : {}),
249
+ };
250
+ emitEvent(emit, 'context.compaction.started', { provider: 'openai', model, previous_response_id: body.previous_response_id || null }, meta);
251
+ let response;
252
+ let http;
253
+ try {
254
+ const result = await request('/responses/compact', body, params);
255
+ response = result.data;
256
+ http = result.http;
257
+ } catch (error) {
258
+ const diagnostic = diagnosticFromError(error, { source: 'openai', kind: 'provider_error' });
259
+ emitEvent(emit, 'error.observed', diagnostic, meta);
260
+ emitEvent(emit, 'run.failed', { stage: 'compaction', provider: 'openai', model, error: diagnostic }, meta);
261
+ throw error;
262
+ }
263
+ emitEvent(emit, 'context.compaction.completed', {
264
+ provider: 'openai', model, compaction_id: response.id, usage: response.usage || null,
265
+ request_id: http?.request_id || null, ray_id: http?.ray_id || null,
266
+ }, meta);
267
+ return Object.freeze({
268
+ provider: 'openai', model, compaction_id: response.id,
269
+ request_id: http?.request_id || null, ray_id: http?.ray_id || null,
270
+ output: Object.freeze(response.output || []), usage: response.usage || null, raw: response,
271
+ });
272
+ }
273
+
274
+ return Object.freeze({ provider: 'openai', create, continueWithToolOutputs, compact });
275
+ }
@@ -4,3 +4,4 @@ export { triageLog, resolveLogFindings } from './logs.js';
4
4
  export { scanProjectSecurity, remediationPlan, reportExitCode } from './scan.js';
5
5
  export { repairProject } from './repair.js';
6
6
  export { formatSecurityReport } from './render.js';
7
+ export { scanTrustBoundary } from './trust-boundary.js';
@@ -76,7 +76,10 @@ export function collectNpmDependencies(projectRoot = process.cwd()) {
76
76
  if (location) {
77
77
  const workspacePath = path.resolve(root, location);
78
78
  const relative = path.relative(root, fs.realpathSync(workspacePath));
79
- if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('Workspace escapes project root');
79
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
80
+ issues.push('Linked package escapes project root and cannot be fully verified here: ' + location);
81
+ continue;
82
+ }
80
83
  const workspace = readJson(path.join(workspacePath, 'package.json'));
81
84
  for (const field of fields) {
82
85
  if (JSON.stringify(Object.entries(workspace[field] || {}).sort()) !== JSON.stringify(Object.entries(pkg[field] || {}).sort())) issues.push('Workspace manifest and lockfile disagree: ' + location + ':' + field);
@@ -1,31 +1,57 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { createProcessDiagnosticError } from '../errors/index.js';
2
3
 
3
4
  export function runProcess(command, args, { cwd, timeoutMs = 300_000, signal, env = process.env, maxBytes = 8 * 1024 * 1024 } = {}) {
4
5
  return new Promise((resolve, reject) => {
5
- if (signal?.aborted) return reject(new Error('Command cancelled'));
6
- const child = spawn(command, args, { cwd, env, shell: false, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] });
6
+ const safeArgs = Array.isArray(args) ? args : [];
7
+ const diagnostic = (code, message, extra = {}) => createProcessDiagnosticError({
8
+ code,
9
+ message,
10
+ command,
11
+ args: safeArgs,
12
+ cwd,
13
+ stdout,
14
+ stderr,
15
+ ...extra,
16
+ });
7
17
  let stdout = '', stderr = '', size = 0, failure, hardKill;
18
+ if (signal?.aborted) return reject(createProcessDiagnosticError({ code: 'process_cancelled', message: 'Command cancelled', command, args: safeArgs, cwd }));
19
+ const child = spawn(command, safeArgs, { cwd, env, shell: false, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] });
8
20
  function kill(sig) {
9
21
  try { process.kill(process.platform === 'win32' ? child.pid : -child.pid, sig); } catch { /* already exited */ }
10
22
  }
11
- function stop(reason) {
23
+ function stop(error) {
12
24
  if (failure) return;
13
- failure = reason;
25
+ failure = error;
14
26
  kill('SIGTERM');
15
27
  hardKill = setTimeout(() => kill('SIGKILL'), 1000);
16
28
  }
17
- const timer = setTimeout(() => stop('Command timed out'), timeoutMs);
18
- const abort = () => stop('Command cancelled');
29
+ const timer = setTimeout(() => stop(diagnostic('process_timeout', 'Command timed out', { retriable: false })), timeoutMs);
30
+ const abort = () => stop(diagnostic('process_cancelled', 'Command cancelled'));
19
31
  signal?.addEventListener('abort', abort, { once: true });
20
32
  const receive = (key) => chunk => {
21
33
  size += chunk.length;
22
- if (size > maxBytes) return stop('Command output exceeded 8 MiB');
34
+ if (size > maxBytes) return stop(diagnostic('process_output_limit', `Command output exceeded ${maxBytes} bytes`));
23
35
  if (key === 'stdout') stdout += chunk.toString(); else stderr += chunk.toString();
24
36
  };
25
37
  child.stdout.on('data', receive('stdout'));
26
38
  child.stderr.on('data', receive('stderr'));
27
39
  const cleanup = () => { clearTimeout(timer); clearTimeout(hardKill); signal?.removeEventListener('abort', abort); };
28
- child.on('error', () => { cleanup(); reject(new Error('Cannot start requested command')); });
29
- child.on('close', code => { cleanup(); failure ? reject(new Error(failure)) : resolve({ code: code ?? 1, stdout, stderr }); });
40
+ child.on('error', (cause) => {
41
+ cleanup();
42
+ reject(createProcessDiagnosticError({
43
+ code: cause?.code || 'process_spawn_failed',
44
+ message: cause?.message || 'Cannot start requested command',
45
+ command,
46
+ args: safeArgs,
47
+ cwd,
48
+ cause,
49
+ }));
50
+ });
51
+ child.on('close', (code, closeSignal) => {
52
+ cleanup();
53
+ if (failure) return reject(failure);
54
+ resolve({ code: code ?? 1, signal: closeSignal || null, stdout, stderr });
55
+ });
30
56
  });
31
57
  }
@@ -1,15 +1,26 @@
1
1
  import { stripVTControlCharacters } from 'node:util';
2
2
 
3
3
  export const safeText = value => stripVTControlCharacters(String(value ?? '')).replace(/[\x00-\x1f\x7f-\x9f]/g, ' ').replace(/[\u202a-\u202e\u2066-\u2069]/g, '');
4
+
5
+ function renderBoundaryFinding(item, lines) {
6
+ const evidence = [item.kind, item.source, item.target ? `→ ${item.target}` : null, item.specifier, item.env].filter(Boolean).join(' · ');
7
+ lines.push(' ! ' + safeText(evidence));
8
+ lines.push(' Next: ' + safeText(item.action || item.repair?.action || 'review-trust-boundary'));
9
+ if (item.repair?.summary) lines.push(' ' + safeText(item.repair.summary));
10
+ }
11
+
4
12
  export function formatSecurityReport(report, { color = false } = {}) {
5
13
  const scan = report.after || report.scan || report.before || report;
6
14
  const label = safeText(report.status || scan.status).toUpperCase();
7
- const title = 'Agent Sam · dependency health · ' + label;
15
+ const title = 'Agent Sam · security · ' + label;
16
+ const boundary = scan.trust_boundary;
8
17
  const lines = [color ? '\x1b[1;36m' + title + '\x1b[0m' : title,
18
+ '',
19
+ 'Dependencies',
9
20
  ' Checked: ' + scan.checked_count + '/' + scan.dependency_count + ' packages',
10
- ' Coverage: ' + (scan.complete ? 'complete' : 'INCOMPLETE — not a clean bill of health')];
21
+ ' Coverage: ' + (scan.dependency_complete ? 'complete' : 'INCOMPLETE — dependency graph not fully verified')];
11
22
  for (const issue of scan.issues || []) lines.push(' ! ' + safeText(issue));
12
- for (const item of scan.findings || []) {
23
+ for (const item of (scan.findings || []).filter((finding) => finding.section !== 'trust-boundary')) {
13
24
  lines.push(' ! ' + safeText([item.kind, item.package, item.version, item.advisory?.id, item.advisory?.severity].filter(Boolean).join(' · ')));
14
25
  lines.push(' Next: ' + safeText(item.action));
15
26
  if (item.advisory?.url) lines.push(' ' + safeText(item.advisory.url));
@@ -17,8 +28,19 @@ export function formatSecurityReport(report, { color = false } = {}) {
17
28
  }
18
29
  const informational = (scan.log_findings || []).filter(f => f.resolution === 'informational').length;
19
30
  if (informational) lines.push(' Informational log notices: ' + informational);
20
- if (report.worktree) lines.push(' Candidate: ' + safeText(report.worktree), ' Branch: ' + safeText(report.branch));
31
+
32
+ if (boundary) {
33
+ lines.push('', 'Trust boundary',
34
+ ' AST files: ' + safeText(boundary.ast_files),
35
+ ' Browser graph: ' + safeText(boundary.browser_reachable_files) + ' reachable files from ' + safeText(boundary.browser_roots) + ' browser roots',
36
+ ' Evidence: merkle=' + safeText(boundary.merkle_root) + ' metadata=' + safeText(boundary.metadata_root),
37
+ ' Coverage: ' + (boundary.complete ? 'complete for indexed JS/TS syntax' : 'INCOMPLETE — parse errors prevent a clean boundary result'));
38
+ for (const item of boundary.findings || []) renderBoundaryFinding(item, lines);
39
+ if (!boundary.findings?.length) lines.push(' ✓ no indexed client/server boundary contradictions detected');
40
+ }
41
+
42
+ if (report.worktree) lines.push('', ' Candidate: ' + safeText(report.worktree), ' Branch: ' + safeText(report.branch));
21
43
  if (report.reason) lines.push(' Reason: ' + safeText(report.reason));
22
- if (report.mode === 'plan') lines.push(' Apply in an isolated worktree: agentsam security repair --apply');
44
+ if (report.mode === 'plan') lines.push(' Apply dependency-only automatic repair in an isolated worktree: agentsam security repair --apply');
23
45
  return lines.join('\n') + '\n';
24
46
  }
@@ -1,38 +1,53 @@
1
1
  import { collectNpmDependencies } from './inventory.js';
2
2
  import { queryOsv } from './osv.js';
3
3
  import { triageLog, resolveLogFindings } from './logs.js';
4
+ import { scanTrustBoundary } from './trust-boundary.js';
4
5
 
5
6
  export async function scanProjectSecurity(options = {}) {
6
7
  const inventory = collectNpmDependencies(options.projectRoot);
7
8
  const results = options.offline
8
9
  ? inventory.dependencies.map(d => ({ ...d, checked: false, advisories: [] }))
9
10
  : await queryOsv(inventory.dependencies, options);
10
- const complete = !options.offline && !options.signal?.aborted && !inventory.issues.length && results.every(r => r.checked);
11
- const logFindings = resolveLogFindings(triageLog(options.log || ''), { complete, results });
12
- const findings = [
13
- ...results.flatMap(r => r.advisories.map(a => ({ kind: 'vulnerability', package: r.name, version: r.version, advisory: a, action: 'audit-fix-within-ranges' }))),
14
- ...results.filter(r => r.deprecated).map(r => ({ kind: 'deprecated', package: r.name, version: r.version, action: 'update-within-ranges' })),
15
- ...logFindings.filter(f => f.resolution === 'action-required'),
11
+ const dependencyComplete = !options.offline && !options.signal?.aborted && !inventory.issues.length && results.every(r => r.checked);
12
+ const scanBoundary = options.scanBoundary || scanTrustBoundary;
13
+ const trustBoundary = await scanBoundary(inventory.root, options);
14
+ const complete = dependencyComplete && trustBoundary.complete;
15
+ const logFindings = resolveLogFindings(triageLog(options.log || ''), { complete: dependencyComplete, results });
16
+ const dependencyFindings = [
17
+ ...results.flatMap(r => r.advisories.map(a => ({ kind: 'vulnerability', section: 'dependencies', package: r.name, version: r.version, advisory: a, action: 'audit-fix-within-ranges' }))),
18
+ ...results.filter(r => r.deprecated).map(r => ({ kind: 'deprecated', section: 'dependencies', package: r.name, version: r.version, action: 'update-within-ranges' })),
19
+ ...logFindings.filter(f => f.resolution === 'action-required').map((f) => ({ ...f, section: 'logs' })),
16
20
  ];
21
+ const boundaryFindings = (trustBoundary.findings || []).map((item) => ({
22
+ ...item,
23
+ section: 'trust-boundary',
24
+ action: item.repair?.action || 'review-trust-boundary',
25
+ }));
26
+ const findings = [...dependencyFindings, ...boundaryFindings];
17
27
  return {
18
28
  schema_version: 1, scanner: 'agentsam-sca', checked_at: new Date().toISOString(),
19
29
  project_root: inventory.root, lockfile: inventory.lockfile, lock_fingerprint: inventory.fingerprint,
20
30
  complete, status: !complete ? 'incomplete' : findings.length ? 'action-required' : 'clean',
21
31
  ok: complete && findings.length === 0,
22
32
  dependency_count: results.length, checked_count: results.filter(r => r.checked).length,
33
+ dependency_complete: dependencyComplete,
23
34
  issues: inventory.issues, skipped: inventory.skipped, results, findings, log_findings: logFindings,
35
+ trust_boundary: trustBoundary,
24
36
  };
25
37
  }
26
38
  export const reportExitCode = report => report.complete === false ? 2 : report.ok ? 0 : 1;
27
39
  export function remediationPlan(report) {
40
+ const boundaryRepairs = report.trust_boundary?.repair_suggestions || [];
28
41
  return {
29
42
  mode: 'plan', ok: report.ok, complete: report.complete, status: report.status,
30
43
  scan: report,
31
44
  steps: [
32
- { action: 'npm-audit-fix', automatic: report.complete, constraints: 'Existing manifest ranges; no force; no lifecycle scripts during resolution.' },
33
- ...(report.findings.some(f => f.kind === 'deprecated') ? [{ action: 'npm-update', automatic: report.complete, constraints: 'Existing ranges; package manifests unchanged.' }] : []),
34
- { action: 'verify', automatic: report.complete, constraints: 'Fresh install, configured project verification script, advisory rescan and warning triage.' },
45
+ { action: 'npm-audit-fix', automatic: report.dependency_complete !== false, constraints: 'Existing manifest ranges; no force; no lifecycle scripts during resolution.' },
46
+ ...(report.findings.some(f => f.kind === 'deprecated') ? [{ action: 'npm-update', automatic: report.dependency_complete !== false, constraints: 'Existing ranges; package manifests unchanged.' }] : []),
47
+ ...boundaryRepairs.map((repair) => ({ ...repair, constraints: 'Preserve the existing public contract where possible; verify the repaired import graph against a fresh Merkle/filemeta snapshot.' })),
48
+ { action: 'verify', automatic: report.complete, constraints: 'Fresh install, configured project verification script, advisory rescan, warning triage, and trust-boundary rescan.' },
35
49
  ],
50
+ boundary_repairs: boundaryRepairs,
36
51
  manual: report.findings.filter(f => !['vulnerability', 'deprecated', 'audit'].includes(f.kind)),
37
52
  };
38
53
  }
@@ -0,0 +1,24 @@
1
+ import fs from 'node:fs';
2
+ import { buildMerkleTree } from '../lib/merkle/index.js';
3
+ import { gitIgnoredPaths } from '../lib/merkle/git-ignore.js';
4
+ import { analyzeExecutionBoundaries } from '../indexing/execution-boundary.js';
5
+
6
+ export async function scanTrustBoundary(projectRoot = process.cwd(), options = {}) {
7
+ const root = fs.realpathSync(projectRoot);
8
+ const ignored = options.exclude || await gitIgnoredPaths(root);
9
+ const snapshot = options.snapshot || await buildMerkleTree(root, { semantic: true, exclude: ignored });
10
+ if (!snapshot.semantic) throw new Error('Semantic Merkle metadata is required for trust-boundary scanning');
11
+ const analysis = analyzeExecutionBoundaries(snapshot.semantic);
12
+ return {
13
+ ...analysis,
14
+ project_root: root,
15
+ merkle_root: snapshot.rootHash,
16
+ metadata_root: snapshot.semantic.rootHash,
17
+ evidence: {
18
+ authority: 'agentsam-merkle+agentsam-filemeta',
19
+ merkle_root: snapshot.rootHash,
20
+ metadata_root: snapshot.semantic.rootHash,
21
+ policy_hash: snapshot.policyHash || null,
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
6
+ const skillsRoot = path.resolve(moduleDir, '..', '..', 'skills');
7
+ const catalogPath = path.join(skillsRoot, 'catalog.json');
8
+
9
+ function readCatalog() {
10
+ return JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
11
+ }
12
+
13
+ function normalizeSkillId(value) {
14
+ return String(value ?? '').trim().toLowerCase();
15
+ }
16
+
17
+ function resolveSkillRecord(idOrAlias) {
18
+ const needle = normalizeSkillId(idOrAlias);
19
+ if (!needle) return null;
20
+ const catalog = readCatalog();
21
+ return catalog.skills.find((skill) => {
22
+ if (normalizeSkillId(skill.id) === needle) return true;
23
+ return (skill.aliases ?? []).some((alias) => normalizeSkillId(alias) === needle);
24
+ }) ?? null;
25
+ }
26
+
27
+ function safeSkillPath(relativePath) {
28
+ const resolved = path.resolve(skillsRoot, relativePath);
29
+ const rootWithSep = `${skillsRoot}${path.sep}`;
30
+ if (resolved !== skillsRoot && !resolved.startsWith(rootWithSep)) {
31
+ throw new Error('skill_path_outside_root');
32
+ }
33
+ return resolved;
34
+ }
35
+
36
+ export function listSkills() {
37
+ return readCatalog().skills.map((skill) => ({ ...skill }));
38
+ }
39
+
40
+ export function getSkill(idOrAlias) {
41
+ const skill = resolveSkillRecord(idOrAlias);
42
+ return skill ? { ...skill } : null;
43
+ }
44
+
45
+ export function loadSkill(idOrAlias, options = {}) {
46
+ const skill = resolveSkillRecord(idOrAlias);
47
+ if (!skill) throw new Error(`unknown_skill:${idOrAlias}`);
48
+
49
+ const result = {
50
+ skill: { ...skill },
51
+ instructions: fs.readFileSync(safeSkillPath(skill.entry), 'utf8'),
52
+ };
53
+
54
+ if (options.references === true) {
55
+ result.references = (skill.references ?? []).map((relativePath) => ({
56
+ path: relativePath,
57
+ content: fs.readFileSync(safeSkillPath(relativePath), 'utf8'),
58
+ }));
59
+ }
60
+
61
+ return result;
62
+ }
63
+
64
+ export const AGENTSAM_SKILLS_ROOT = skillsRoot;