@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
package/src/ui/theme.js CHANGED
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * @file src/ui/theme.js
3
- * @description IAM Terminal Theme System — single source of truth for
4
- * per-lane color identity. Used by:
5
- * - xterm.js panels in XTermShell.tsx (pass theme object to Terminal constructor)
6
- * - CLI ANSI output in agentsam-sdk (import ANSI constants)
7
- * - Splash/HUD renders in both contexts
3
+ * @description Agent Sam terminal theme system — portable color and semantic
4
+ * status tokens for native ANSI output and browser/xterm consumers.
8
5
  *
9
- * Lane identity:
10
- * LOCAL → amber (samsmac tunnel, darwin)
11
- * CLOUD → teal (GCP platform_vm, iam-pty)
12
- * SANDBOX → purple (CF container, isolated exec)
6
+ * Lane identity is intentionally provider-neutral:
7
+ * LOCAL → amber (the user's current machine / real shell)
8
+ * REMOTE → teal (a connected remote runtime, provider chosen by the host)
9
+ * SANDBOX → purple (an isolated/disposable runtime, provider chosen by the host)
10
+ *
11
+ * Infrastructure names, domains, machine names, and account-specific routing
12
+ * never belong in this SDK theme. Hosts may supply their own runtime labels.
13
13
  *
14
14
  * No dependencies. ESM + CJS compatible (conditional exports in package.json).
15
15
  */
@@ -31,7 +31,7 @@ export const PALETTE = {
31
31
  ghost: '#aaaaaa',
32
32
  white: '#e8e8e8',
33
33
 
34
- // LOCAL lane — amber/gold (samsmac, darwin_arm64)
34
+ // LOCAL lane — amber/gold (current machine / real shell)
35
35
  amber900: '#0d0900',
36
36
  amber800: '#1a1200',
37
37
  amber700: '#2d1f00',
@@ -43,7 +43,7 @@ export const PALETTE = {
43
43
  amber100: '#fde68a',
44
44
  amber050: '#fef3c7',
45
45
 
46
- // CLOUD lane — teal/cyan (GCP, iam-pty, platform_vm)
46
+ // REMOTE lane — teal/cyan (provider-neutral connected runtime)
47
47
  teal900: '#000d0b',
48
48
  teal800: '#001a17',
49
49
  teal700: '#002e28',
@@ -55,7 +55,7 @@ export const PALETTE = {
55
55
  teal100: '#99f6e4',
56
56
  teal050: '#ccfbf1',
57
57
 
58
- // SANDBOX lane — purple (CF container, isolated build exec)
58
+ // SANDBOX lane — purple (provider-neutral isolated runtime)
59
59
  purple900: '#06020d',
60
60
  purple800: '#0d0520',
61
61
  purple700: '#1e0a3d',
@@ -130,26 +130,26 @@ const makeLane = ({ bg, bgSubtle, primary, secondary, dim, accent, name, label,
130
130
  });
131
131
 
132
132
  // ─────────────────────────────────────────────────────────────────────────────
133
- // THE THREE LANES
133
+ // PORTABLE EXECUTION LANES
134
134
  // ─────────────────────────────────────────────────────────────────────────────
135
135
 
136
136
  export const LOCAL = makeLane({
137
137
  name: 'local', label: 'Local', icon: '◈',
138
- tunnelHint: 'samsmac → localpty.inneranimalmedia.com',
138
+ tunnelHint: 'current machine · real shell',
139
139
  bg: '#0d0900', bgSubtle: '#1a1200',
140
140
  primary: '#f59e0b', secondary: '#fbbf24', dim: '#78450a', accent: '#22d3c8',
141
141
  });
142
142
 
143
143
  export const CLOUD = makeLane({
144
- name: 'cloud', label: 'Cloud', icon: '●',
145
- tunnelHint: 'inneranimalmedia → terminal.inneranimalmedia.com (GCP iam-tunnel)',
144
+ name: 'remote', label: 'Remote', icon: '●',
145
+ tunnelHint: 'connected remote runtime',
146
146
  bg: '#000d0b', bgSubtle: '#001a17',
147
147
  primary: '#22d3c8', secondary: '#5eead4', dim: '#065f52', accent: '#f59e0b',
148
148
  });
149
149
 
150
150
  export const SANDBOX = makeLane({
151
151
  name: 'sandbox', label: 'Sandbox', icon: '◌',
152
- tunnelHint: 'CF Container → MY_CONTAINER DO (inneranimalmedia)',
152
+ tunnelHint: 'isolated disposable runtime',
153
153
  bg: '#06020d', bgSubtle: '#0d0520',
154
154
  primary: '#a78bfa', secondary: '#c4b5fd', dim: '#4c1d95', accent: '#22d3c8',
155
155
  });
@@ -173,11 +173,12 @@ export function getLaneTheme(targetType) {
173
173
 
174
174
  /**
175
175
  * Get lane theme by name string (for SDK CLI use).
176
- * @param {'local'|'cloud'|'sandbox'|string} name
176
+ * @param {'local'|'remote'|'cloud'|'sandbox'|string} name
177
177
  */
178
178
  export function getLaneThemeByName(name) {
179
179
  switch (name?.toLowerCase()) {
180
180
  case 'local': return LOCAL;
181
+ case 'remote':
181
182
  case 'cloud': return CLOUD;
182
183
  case 'sandbox': return SANDBOX;
183
184
  default: return CLOUD;
@@ -0,0 +1,36 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+ import { accountSessionPath, clearAccountSession, readAccountSession, resolveAccountSdkKey, saveAccountSession } from '../src/lib/account-session.js';
7
+
8
+ function tempHome(t) {
9
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-account-'));
10
+ t.after(() => fs.rmSync(home, { recursive: true, force: true }));
11
+ return home;
12
+ }
13
+
14
+ test('browser-auth SDK bearer persists in machine-local storage, not project state', t => {
15
+ const home = tempHome(t);
16
+ const saved = saveAccountSession({ access_token: 'sdk_machine_session', user_id: 'au_test', account_id: 'acct_test', email: 'dev@example.test' }, { home });
17
+ assert.equal(saved.user_id, 'au_test');
18
+ const filename = accountSessionPath({ home });
19
+ assert.equal(fs.existsSync(filename), true);
20
+ if (process.platform !== 'win32') assert.equal(fs.statSync(filename).mode & 0o077, 0);
21
+
22
+ const loaded = readAccountSession({ home });
23
+ assert.equal(loaded.sdk_key, 'sdk_machine_session');
24
+ const resolved = resolveAccountSdkKey({ env: {}, home });
25
+ assert.equal(resolved.source, 'agentsam_account_session');
26
+ assert.equal(resolved.value, 'sdk_machine_session');
27
+ assert.equal(clearAccountSession({ home }), true);
28
+ assert.equal(readAccountSession({ home }), null);
29
+ });
30
+
31
+ test('explicit/environment SDK bearer remains higher authority than local session fallback', t => {
32
+ const home = tempHome(t);
33
+ saveAccountSession({ access_token: 'sdk_disk' }, { home });
34
+ assert.equal(resolveAccountSdkKey({ env: { AGENTSAM_SDK_KEY: 'sdk_env' }, home }).value, 'sdk_env');
35
+ assert.equal(resolveAccountSdkKey({ env: { AGENTSAM_SDK_KEY: 'sdk_env' }, explicit: 'sdk_explicit', home }).value, 'sdk_explicit');
36
+ });
@@ -0,0 +1,61 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+ import { getSkill, listSkills, loadSkill } from '../src/skills/index.js';
7
+
8
+ const root = path.resolve(import.meta.dirname, '..');
9
+ const cli = path.join(root, 'src', 'cli.js');
10
+
11
+ test('application fundamentals and progression guard are portable registered skills', () => {
12
+ const ids = new Set(listSkills().map((skill) => skill.id));
13
+ assert.ok(ids.has('agentsam-app-fundamentals'));
14
+ assert.ok(ids.has('agentsam-progression-guard'));
15
+ assert.equal(getSkill('quick-bytes')?.id, 'agentsam-app-fundamentals');
16
+ assert.equal(getSkill('no-regress')?.id, 'agentsam-progression-guard');
17
+
18
+ const fundamentals = loadSkill('quick-bytes', { references: true });
19
+ assert.equal(fundamentals.references.length, 2);
20
+ assert.match(fundamentals.instructions, /Credentialed-destination card/);
21
+ assert.match(fundamentals.instructions, /AST/);
22
+ assert.match(fundamentals.instructions, /Merkle/);
23
+ assert.match(fundamentals.instructions, /Firewall\/WAF/);
24
+ assert.match(fundamentals.references[0].content, /OAuth redirect mental model/);
25
+ assert.match(fundamentals.references[1].content, /The codebase is a graph/);
26
+
27
+ const guard = loadSkill('no-regress', { references: true });
28
+ assert.equal(guard.references.length, 2);
29
+ assert.match(guard.instructions, /failed candidate must not advance the last-known-good baseline/);
30
+ assert.match(guard.instructions, /agentsam deploy-receipt capture/);
31
+ assert.match(guard.references[0].content, /Gate 6 — postdeploy/);
32
+ assert.match(guard.references[1].content, /Ingress versus egress hooks/);
33
+ });
34
+
35
+ test('skills CLI lists compact cards and loads quick reminders by alias', () => {
36
+ const listed = spawnSync(process.execPath, [cli, 'skills', '--json'], { cwd: root, encoding: 'utf8' });
37
+ assert.equal(listed.status, 0, listed.stderr);
38
+ const catalog = JSON.parse(listed.stdout);
39
+ assert.ok(catalog.some((skill) => skill.id === 'agentsam-app-fundamentals'));
40
+ assert.ok(catalog.some((skill) => skill.id === 'agentsam-progression-guard'));
41
+
42
+ const loaded = spawnSync(process.execPath, [cli, 'skills', 'quick-bytes'], { cwd: root, encoding: 'utf8' });
43
+ assert.equal(loaded.status, 0, loaded.stderr);
44
+ assert.match(loaded.stdout, /# AgentSam Application Fundamentals/);
45
+ assert.doesNotMatch(loaded.stdout, /# Trust, Credentials, and Credentialed Destinations/);
46
+
47
+ const full = spawnSync(process.execPath, [cli, 'skills', 'no-regress', '--references'], { cwd: root, encoding: 'utf8' });
48
+ assert.equal(full.status, 0, full.stderr);
49
+ assert.match(full.stdout, /# AgentSam Progression Guard/);
50
+ assert.match(full.stdout, /# Checkpoint Chain/);
51
+ assert.match(full.stdout, /# Hooks and Operational I\/O/);
52
+ });
53
+
54
+ test('portable npm package still owns all skill files', () => {
55
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
56
+ assert.ok(pkg.files.includes('skills'));
57
+ for (const rel of [
58
+ 'skills/agentsam-app-fundamentals/SKILL.md',
59
+ 'skills/agentsam-progression-guard/SKILL.md',
60
+ ]) assert.equal(fs.existsSync(path.join(root, rel)), true, `${rel} must exist`);
61
+ });
@@ -0,0 +1,56 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import test from 'node:test';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
+ const productApps = ['local-studio', 'cad-creator', 'client-cms-editor'];
9
+ const at = (...parts) => path.join(root, ...parts);
10
+ const exists = (...parts) => fs.existsSync(at(...parts));
11
+
12
+ function packageJson(...parts) {
13
+ return JSON.parse(fs.readFileSync(at(...parts, 'package.json'), 'utf8'));
14
+ }
15
+
16
+ test('product apps are self-contained npm workspace roots with one lockfile', () => {
17
+ for (const app of productApps) {
18
+ const base = ['apps', app];
19
+ assert.ok(exists(...base, 'package.json'), `${app}: root package.json`);
20
+ assert.ok(exists(...base, 'package-lock.json'), `${app}: root package-lock.json`);
21
+ assert.ok(exists(...base, 'frontend', 'package.json'), `${app}: frontend package`);
22
+ assert.ok(exists(...base, 'backend', 'package.json'), `${app}: backend package`);
23
+ assert.equal(exists(...base, 'frontend', 'package-lock.json'), false, `${app}: no frontend lockfile`);
24
+ assert.equal(exists(...base, 'backend', 'package-lock.json'), false, `${app}: no backend lockfile`);
25
+
26
+ const pkg = packageJson(...base);
27
+ assert.deepEqual(pkg.workspaces, ['frontend', 'backend', 'shared/*'], `${app}: canonical workspace list`);
28
+ }
29
+ });
30
+
31
+ test('every product app owns its Cloudflare boundary under backend/', () => {
32
+ for (const app of productApps) {
33
+ const base = ['apps', app, 'backend'];
34
+ assert.ok(exists(...base, 'worker', 'index.js'), `${app}: backend/worker/index.js`);
35
+ assert.ok(exists(...base, 'wrangler.jsonc'), `${app}: backend/wrangler.jsonc`);
36
+ const wrangler = fs.readFileSync(at(...base, 'wrangler.jsonc'), 'utf8');
37
+ assert.match(wrangler, /"main"\s*:\s*"worker\/index\.js"/, `${app}: wrangler main`);
38
+ }
39
+ });
40
+
41
+ test('SDK root is package tooling only and does not own an app Worker', () => {
42
+ assert.equal(exists('wrangler.jsonc'), false);
43
+ assert.equal(exists('worker', 'index.js'), false);
44
+ assert.equal(exists('scripts', 'with-cloudflare-env.sh'), false);
45
+
46
+ const rootPkg = packageJson();
47
+ for (const workspace of rootPkg.workspaces || []) {
48
+ assert.equal(workspace.startsWith('apps/'), false, `root workspace must not absorb app workspace: ${workspace}`);
49
+ }
50
+ });
51
+
52
+ test('Local Studio retired donor-era duplicate deployment configs', () => {
53
+ for (const file of ['wrangler.ui.toml', 'wrangler.workmode.toml', 'wrangler.toml.example']) {
54
+ assert.equal(exists('apps', 'local-studio', 'backend', file), false, `retire ${file}`);
55
+ }
56
+ });
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import { execFileSync } from 'node:child_process';
7
7
  import { getCapability, getCapabilityManifest, repositorySnapshot } from '../src/capabilities/index.js';
8
8
  import { getPreset, resolvePreset } from '../src/presets/index.js';
9
+ import { scanTrustBoundary } from '../src/security/trust-boundary.js';
9
10
 
10
11
  const CLI = path.resolve('src/cli.js');
11
12
 
@@ -51,11 +52,51 @@ test('repository.snapshot composes deterministic evidence and content addressing
51
52
  assert.equal(one.snapshot_id, two.snapshot_id);
52
53
  assert.equal(one.repository.revision_sha, git(root, ['rev-parse', 'HEAD']));
53
54
  assert.ok(one.tree.merkle_root);
55
+ assert.match(one.tree.metadata_root, /^sha256:[a-f0-9]{64}$/);
56
+ assert.equal(one.tree.manifest.format, 'agentsam-merkle');
57
+ assert.equal(one.tree.manifest.version, 1);
58
+ assert.equal(one.tree.manifest.hash_algorithm, 'sha256');
59
+ assert.match(one.tree.manifest.policy_hash, /^sha256:[a-f0-9]{64}$/);
60
+ assert.equal(one.tree.classifier.format, 'agentsam-filemeta');
61
+ assert.equal(one.tree.classifier.version, 1);
62
+ const source = one.tree.files.find((entry) => entry.path === 'src/index.js');
63
+ assert.equal(source.mode, 420);
64
+ assert.equal(source.system, 'snapshot-fixture');
65
+ assert.equal(source.language, 'javascript');
66
+ assert.deepEqual(source.symbols, ['value']);
67
+ assert.equal(one.analysis.trust_boundary.analyzer, 'agentsam-execution-boundary');
68
+ assert.equal(one.analysis.trust_boundary.metadata_root, one.tree.metadata_root);
69
+ assert.equal(one.analysis.trust_boundary.complete, true);
70
+ assert.equal(one.analysis.trust_boundary.status, 'clean');
54
71
  assert.ok(one.intelligence.summary.file_count >= 2);
55
72
  assert.equal(one.packages[0].name, 'snapshot-fixture');
56
73
  assert.equal(one.knowledge.configured, false);
57
74
  });
58
75
 
76
+ test('repository inspection and security scan share one Git-ignore/Merkle trust authority', async t => {
77
+ const root = fixture();
78
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
79
+ fs.mkdirSync(path.join(root, 'frontend'));
80
+ fs.mkdirSync(path.join(root, 'backend'));
81
+ fs.mkdirSync(path.join(root, 'ignored'));
82
+ fs.writeFileSync(path.join(root, 'frontend', 'client.ts'), "import { secretValue } from '../backend/secrets.ts';\nexport const value = secretValue;\n");
83
+ fs.writeFileSync(path.join(root, 'backend', 'secrets.ts'), "export const secretValue = process.env.API_KEY;\n");
84
+ fs.writeFileSync(path.join(root, 'ignored', 'noise.ts'), "import fs from 'node:fs'; export const x = process.env.PRIVATE_TOKEN + String(fs);\n");
85
+ fs.writeFileSync(path.join(root, '.gitignore'), 'ignored/\n');
86
+
87
+ const snapshot = await repositorySnapshot({ cwd: root, churnDays: 30 });
88
+ const boundary = await scanTrustBoundary(root);
89
+ assert.equal(snapshot.tree.metadata_root, boundary.metadata_root);
90
+ assert.equal(snapshot.tree.merkle_root, boundary.merkle_root);
91
+ assert.equal(snapshot.analysis.trust_boundary.status, 'action-required');
92
+ assert.deepEqual(
93
+ snapshot.analysis.trust_boundary.findings.map((item) => [item.kind, item.source, item.target]),
94
+ boundary.findings.map((item) => [item.kind, item.source, item.target]),
95
+ );
96
+ assert.equal(snapshot.tree.paths.some((value) => value.startsWith('ignored/')), false);
97
+ assert.equal(boundary.findings.some((item) => item.source.startsWith('ignored/')), false);
98
+ });
99
+
59
100
  test('product UX creates a preset project, adds a feature, and inspects before first commit', t => {
60
101
  const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-product-'));
61
102
  t.after(() => fs.rmSync(parent, { recursive: true, force: true }));
@@ -65,10 +106,15 @@ test('product UX creates a preset project, adds a feature, and inspects before f
65
106
  });
66
107
  const project = path.join(parent, 'demo');
67
108
  const config = JSON.parse(fs.readFileSync(path.join(project, '.agentsam', 'config.json'), 'utf8'));
68
- assert.equal(config.preset, 'cms');
69
- assert.equal(config.lane, 'cms');
70
- assert.deepEqual(config.features, ['cms', 'knowledge']);
71
- assert.ok(config.capabilities.includes('repository.snapshot'));
109
+ assert.equal(config.schema_version, 2);
110
+ assert.equal(config.project.name, 'demo');
111
+ assert.equal(config.product.preset, 'cms');
112
+ assert.deepEqual(config.product.features, ['cms', 'knowledge']);
113
+ assert.ok(config.product.capabilities.includes('repository.snapshot'));
114
+ assert.match(config.repository.id, /^local:[0-9a-f-]{36}$/);
115
+ assert.equal(config.defaults.runtime, 'local');
116
+ assert.equal(config.defaults.model, 'auto');
117
+ assert.equal(fs.existsSync(path.join(project, 'agentsam.config.js')), false);
72
118
 
73
119
  execFileSync(process.execPath, [CLI, 'add', 'knowledge', '--cwd', project, '--json'], { stdio: 'pipe' });
74
120
  const features = JSON.parse(fs.readFileSync(path.join(project, '.agentsam', 'features.json'), 'utf8'));
@@ -79,6 +125,9 @@ test('product UX creates a preset project, adds a feature, and inspects before f
79
125
  stdio: ['ignore', 'pipe', 'pipe'],
80
126
  }));
81
127
  assert.equal(snapshot.capability, 'repository.snapshot');
128
+ assert.equal(snapshot.repository.repository_id, config.repository.id);
129
+ assert.equal(snapshot.repository.identity_source, 'project-config');
82
130
  assert.equal(snapshot.repository.revision_sha, null);
83
131
  assert.ok(snapshot.tree.stats.files > 0);
132
+ assert.ok(snapshot.analysis?.trust_boundary);
84
133
  });
@@ -0,0 +1,46 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+ import { CLI_PREFERENCES_SCHEMA, detectCliProject, readCliPreferences, writeCliPreferences } from '../src/lib/cli-preferences.js';
7
+ import { renderBootSummary } from '../src/ui/boot.js';
8
+
9
+ test('CLI preferences persist local model/runtime controls without becoming routing authority', () => {
10
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-cli-prefs-'));
11
+ fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'demo-project' }));
12
+ const written = writeCliPreferences(root, {
13
+ trustedDirectory: true,
14
+ runtime: 'sandbox',
15
+ terminal: 'zsh',
16
+ modelPreference: 'openai:gpt-6-astra',
17
+ reasoningEffort: 'high',
18
+ serviceTier: 'fast',
19
+ });
20
+ assert.equal(written.schemaVersion, CLI_PREFERENCES_SCHEMA);
21
+ assert.equal(written.modelAuthority, 'preference-only');
22
+ assert.equal(written.trustedDirectory, true);
23
+ assert.deepEqual(readCliPreferences(root), written);
24
+ const identity = detectCliProject(root);
25
+ assert.equal(identity.project, 'demo-project');
26
+ assert.equal(identity.root, root);
27
+ const summary = renderBootSummary({ identity, preferences: written });
28
+ assert.match(summary, /Agent Sam/);
29
+ assert.match(summary, /demo-project/);
30
+ assert.match(summary, /openai:gpt-6-astra/);
31
+ assert.match(summary, /high/);
32
+ assert.match(summary, /fast/);
33
+ });
34
+
35
+ test('legacy v1 local preferences migrate in memory without inventing trust', () => {
36
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-cli-legacy-'));
37
+ fs.mkdirSync(path.join(root, '.agentsam'));
38
+ fs.writeFileSync(path.join(root, '.agentsam', 'cli.json'), JSON.stringify({
39
+ schemaVersion: 'agentsam-cli-preferences-v1', runtime: 'local', terminal: 'zsh', modelPreference: 'auto', modelAuthority: 'preference-only',
40
+ }));
41
+ const read = readCliPreferences(root);
42
+ assert.equal(read.schemaVersion, CLI_PREFERENCES_SCHEMA);
43
+ assert.equal(read.trustedDirectory, false);
44
+ assert.equal(read.reasoningEffort, 'auto');
45
+ assert.equal(read.serviceTier, 'default');
46
+ });
@@ -0,0 +1,96 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import { handleCloudflareConnectionRequest } from '../packages/connectors/cloudflare/src/routes.js';
4
+ import { rejectUntrustedOwnerHints } from '../packages/connectors/cloudflare/src/owner.js';
5
+
6
+ function req(url, { method = 'GET', headers = {}, body } = {}) {
7
+ return new Request(url, { method, headers, body });
8
+ }
9
+
10
+ describe('cloudflare connector routes', () => {
11
+ it('rejects unauthenticated status', async () => {
12
+ const res = await handleCloudflareConnectionRequest(
13
+ req('https://agentsam.inneranimalmedia.com/api/connections/cloudflare'),
14
+ {},
15
+ );
16
+ assert.equal(res.status, 401);
17
+ const json = await res.json();
18
+ assert.equal(json.error, 'unauthenticated');
19
+ });
20
+
21
+ it('rejects browser-submitted owner hints', () => {
22
+ const url = new URL('https://agentsam.inneranimalmedia.com/api/connections/cloudflare?account_id=evil');
23
+ assert.throws(
24
+ () => rejectUntrustedOwnerHints(req(url.toString()), url, {}),
25
+ /untrusted_owner_hint/,
26
+ );
27
+ });
28
+
29
+ it('returns 503 on start when only fixture credentials exist', async () => {
30
+ const env = {
31
+ CLOUDFLARE_OAUTH_CLIENT_ID: 'sillynotreal',
32
+ CLOUDFLARE_OAUTH_CLIENT_SECRET: 'sillynotreal-secret',
33
+ sessions: new Map([['sess_1', 'user-sam']]),
34
+ };
35
+ const res = await handleCloudflareConnectionRequest(
36
+ req('https://agentsam.inneranimalmedia.com/api/connections/cloudflare/start', {
37
+ headers: { cookie: 'agentsam_session=sess_1' },
38
+ }),
39
+ env,
40
+ );
41
+ assert.equal(res.status, 503);
42
+ const json = await res.json();
43
+ assert.equal(json.error, 'not_configured');
44
+ assert.equal(json.fixture, true);
45
+ });
46
+
47
+ it('status stays safe and does not treat fixture as production configured', async () => {
48
+ const env = {
49
+ CLOUDFLARE_OAUTH_CLIENT_ID: 'sillynotreal',
50
+ CLOUDFLARE_OAUTH_CLIENT_SECRET: 'sillynotreal-secret',
51
+ sessions: new Map([['sess_1', 'user-sam']]),
52
+ };
53
+ const res = await handleCloudflareConnectionRequest(
54
+ req('https://agentsam.inneranimalmedia.com/api/connections/cloudflare', {
55
+ headers: { authorization: 'Bearer sess_1' },
56
+ }),
57
+ env,
58
+ );
59
+ assert.equal(res.status, 200);
60
+ const json = await res.json();
61
+ assert.equal(json.ok, true);
62
+ assert.equal(json.configured, false);
63
+ assert.equal(json.fixture, true);
64
+ assert.equal(JSON.stringify(json).includes('sillynotreal-secret'), false);
65
+ });
66
+
67
+ it('rejects owner_id in the JSON body', async () => {
68
+ const env = { sessions: new Map([['sess_1', 'user-sam']]) };
69
+ const res = await handleCloudflareConnectionRequest(
70
+ req('https://agentsam.inneranimalmedia.com/api/connections/cloudflare/disconnect', {
71
+ method: 'POST',
72
+ headers: { cookie: 'agentsam_session=sess_1', 'content-type': 'application/json' },
73
+ body: JSON.stringify({ owner_id: 'evil' }),
74
+ }),
75
+ env,
76
+ );
77
+ assert.equal(res.status, 400);
78
+ const json = await res.json();
79
+ assert.equal(json.error, 'untrusted_owner_hint');
80
+ });
81
+
82
+ it('rejects callback without a stored oauth state', async () => {
83
+ const env = {
84
+ CLOUDFLARE_OAUTH_CLIENT_ID: 'real-client-id',
85
+ CLOUDFLARE_OAUTH_CLIENT_SECRET: 'real-client-secret-value',
86
+ oauthState: new Map(),
87
+ };
88
+ const res = await handleCloudflareConnectionRequest(
89
+ req('https://agentsam.inneranimalmedia.com/api/connections/cloudflare/callback?code=abc&state=missing'),
90
+ env,
91
+ );
92
+ assert.equal(res.status, 403);
93
+ const json = await res.json();
94
+ assert.equal(json.error, 'cloudflare_connection_forbidden');
95
+ });
96
+ });
@@ -0,0 +1,75 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import test from 'node:test';
6
+ import { buildWranglerInvocation, listWranglerNativeCommands, runWranglerNative, summarizeCloudflareCpuProfile, summarizeCloudflareCpuProfileFile } from '../src/cloudflare/index.js';
7
+ import { createCapabilityAdapter } from '../src/agent/capability-adapter.js';
8
+
9
+ function tempRoot() { return fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-cf-')); }
10
+
11
+ test('Cloudflare native command catalog exposes bounded read operations and never an auth-token secret read', () => {
12
+ const ids = listWranglerNativeCommands().map((row) => row.id);
13
+ assert.deepEqual(ids, ['whoami', 'deployments.list', 'versions.list', 'types.check', 'queues.list']);
14
+ assert.ok(!ids.some((id) => id.includes('token') || id.includes('secret') || id === 'deploy'));
15
+ });
16
+
17
+ test('Wrangler native invocation is argv-based and cwd/config scoped', () => {
18
+ const root = tempRoot();
19
+ fs.writeFileSync(path.join(root, 'wrangler.jsonc'), '{}');
20
+ const plan = buildWranglerInvocation('deployments.list', { cwd: root, name: 'demo' });
21
+ assert.equal(plan.cwd, root);
22
+ assert.deepEqual(plan.args.slice(0, 4), ['deployments', 'list', '--json', '--name']);
23
+ assert.equal(plan.args[4], 'demo');
24
+ assert.ok(plan.args.includes('--config'));
25
+ assert.throws(() => buildWranglerInvocation('whoami', { cwd: root, config: '../outside.toml' }), /outside_cwd/);
26
+ });
27
+
28
+ test('Wrangler native result preserves JSON and real exit code on failure', async () => {
29
+ const root = tempRoot();
30
+ const success = await runWranglerNative('whoami', { cwd: root }, { run: async () => ({ code: 0, stdout: JSON.stringify({ email: 'dev@example.com' }), stderr: '' }) });
31
+ assert.equal(success.data.email, 'dev@example.com');
32
+ await assert.rejects(
33
+ runWranglerNative('versions.list', { cwd: root }, { run: async () => ({ code: 7, stdout: '', stderr: 'upstream failed' }) }),
34
+ (error) => error.diagnostic?.exit_code === 7 && error.diagnostic?.code === 'wrangler_exit_nonzero',
35
+ );
36
+ await assert.rejects(
37
+ runWranglerNative('deployments.list', { cwd: root }, { run: async () => ({ code: 1, stdout: '', stderr: 'Authentication error [code: 10000]\nRequest ID: req_cf_1\nCF-Ray: ray-123' }) }),
38
+ (error) => error.diagnostic?.code === '10000' && error.diagnostic?.request_id === 'req_cf_1' && error.diagnostic?.ray_id === 'ray-123',
39
+ );
40
+ });
41
+
42
+ test('CPU profile summary ranks self-time hotspots and explicitly rejects production timer inference', () => {
43
+ const profile = {
44
+ nodes: [
45
+ { id: 1, callFrame: { functionName: 'fetch', url: 'worker.js', lineNumber: 0, columnNumber: 0 } },
46
+ { id: 2, callFrame: { functionName: 'heavyLoop', url: 'worker.js', lineNumber: 10, columnNumber: 2 } },
47
+ { id: 3, callFrame: { functionName: '(garbage collector)', url: '', lineNumber: -1, columnNumber: -1 } },
48
+ ],
49
+ samples: [1, 2, 2, 3],
50
+ timeDeltas: [100, 1200, 800, 400],
51
+ };
52
+ const summary = summarizeCloudflareCpuProfile(profile);
53
+ assert.equal(summary.top_frames[0].function, 'heavyLoop');
54
+ assert.equal(summary.top_frames[0].self_us, 2000);
55
+ assert.equal(summary.garbage_collection_ms, 0.4);
56
+ assert.match(summary.timer_semantics, /do not advance during CPU-only execution/);
57
+ });
58
+
59
+ test('CPU profile file cannot escape runtime cwd', () => {
60
+ const root = tempRoot();
61
+ fs.writeFileSync(path.join(root, 'profile.cpuprofile'), JSON.stringify({ nodes: [], samples: [], timeDeltas: [] }));
62
+ assert.equal(summarizeCloudflareCpuProfileFile({ cwd: root, file: 'profile.cpuprofile' }).file, 'profile.cpuprofile');
63
+ assert.throws(() => summarizeCloudflareCpuProfileFile({ cwd: root, file: '../profile.cpuprofile' }), /outside_cwd/);
64
+ });
65
+
66
+ test('explicitly executable experimental Cloudflare capabilities enter cards-first discovery without exposing unavailable commands', () => {
67
+ const adapter = createCapabilityAdapter();
68
+ const rows = adapter.toolDescriptors();
69
+ const ids = rows.map((row) => row.name);
70
+ assert.ok(ids.includes('cloudflare.wrangler.native'));
71
+ assert.ok(ids.includes('cloudflare.cpu.profile'));
72
+ assert.ok(!ids.includes('cloudflare.cpu.audit'));
73
+ assert.ok(!ids.includes('blender.build'));
74
+ assert.equal(rows.find((row) => row.name === 'cloudflare.wrangler.native').risk, 'read');
75
+ });