@inneranimalmedia/agentsam-sdk 2.4.0 → 2.5.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 (162) hide show
  1. package/README.md +7 -6
  2. package/docs/AGENTSAM_WORKBENCH.md +30 -0
  3. package/docs/AUTH_IDENTITY_CONTRACT.md +52 -0
  4. package/docs/CAPABILITIES.md +5 -3
  5. package/docs/CLI_SHELL.md +57 -80
  6. package/docs/CMS_STUDIO.md +101 -0
  7. package/docs/CONTEXT.md +170 -0
  8. package/docs/LOCAL_OLLAMA.md +58 -0
  9. package/docs/MERKLE.md +19 -19
  10. package/docs/PORTABLE_CONTEXT.md +4 -3
  11. package/docs/PROJECT_CONFIG.md +72 -0
  12. package/docs/RELEASES.md +26 -2
  13. package/docs/REPOSITORY_INTELLIGENCE.md +1 -1
  14. package/docs/REPOSITORY_KNOWLEDGE.md +114 -0
  15. package/docs/SDK_WORKER.md +86 -0
  16. package/docs/SECURITY.md +59 -22
  17. package/docs/client-cms-editor.md +15 -0
  18. package/docs/local-studio/WORKMODE_DONOR_NOTES.md +485 -0
  19. package/package.json +17 -5
  20. package/packages/identity/package.json +11 -2
  21. package/packages/identity/src/contracts/auth-config.js +98 -0
  22. package/packages/identity/src/index.js +1 -0
  23. package/packages/identity/src/oauth/README.md +2 -2
  24. package/packages/identity/src/oauth/credentials.js +11 -4
  25. package/packages/identity/src/oauth/iam-platform.js +3 -3
  26. package/packages/identity/src/providers/iam/index.js +7 -7
  27. package/packages/identity/src/providers/iam/oauth.js +6 -4
  28. package/packages/identity/src/providers/iam/profile.js +5 -5
  29. package/packages/identity/tests/auth-config.test.mjs +57 -0
  30. package/packages/identity/tests/oauth-credentials.test.mjs +13 -2
  31. package/protocol/FILEMETA_V1.md +95 -0
  32. package/protocol/INSPECT_VIEWS_V1.md +40 -0
  33. package/protocol/MERKLE_PERSISTENCE_V1.md +50 -0
  34. package/protocol/MERKLE_V1.md +3 -1
  35. package/protocol/capabilities/manifest.json +32 -3
  36. package/protocol/capabilities/repository-snapshot.schema.json +1 -0
  37. package/protocol/context/context-budget.schema.json +41 -0
  38. package/protocol/context/context-item.schema.json +20 -0
  39. package/protocol/context/resolved-context-pack.schema.json +42 -0
  40. package/protocol/context/result-policy.schema.json +17 -0
  41. package/protocol/knowledge/context-pack.schema.json +33 -0
  42. package/protocol/knowledge/retrieval-query.schema.json +68 -13
  43. package/python/README.md +15 -7
  44. package/python/agentsam_sdk/cli.py +0 -21
  45. package/python/agentsam_sdk/tui/README.md +17 -12
  46. package/python/agentsam_sdk/tui/bootstrap.py +2 -2
  47. package/python/agentsam_sdk/tui/demo.py +25 -10
  48. package/python/agentsam_sdk/tui/onboarding.py +208 -0
  49. package/python/tests/test_tui_cli.py +7 -6
  50. package/skills/README.md +21 -0
  51. package/skills/agentsam-app-fundamentals/SKILL.md +165 -0
  52. package/skills/agentsam-app-fundamentals/references/graphs-contracts-ast-merkle.md +89 -0
  53. package/skills/agentsam-app-fundamentals/references/trust-credentials-and-destinations.md +99 -0
  54. package/skills/agentsam-jr-dev/SKILL.md +232 -0
  55. package/skills/agentsam-jr-dev/references/real-application-logic.md +156 -0
  56. package/skills/agentsam-jr-dev/references/web-application-fundamentals.md +240 -0
  57. package/skills/agentsam-progression-guard/SKILL.md +197 -0
  58. package/skills/agentsam-progression-guard/references/checkpoint-chain.md +111 -0
  59. package/skills/agentsam-progression-guard/references/hooks-operational-io.md +96 -0
  60. package/skills/catalog.json +53 -0
  61. package/src/capabilities/index.js +7 -0
  62. package/src/capabilities/repository-snapshot-view.js +238 -0
  63. package/src/capabilities/repository-snapshot.js +28 -14
  64. package/src/cli.js +108 -56
  65. package/src/commands/cad.js +56 -6
  66. package/src/commands/context.js +14 -2
  67. package/src/commands/db.js +4 -7
  68. package/src/commands/deploy.js +17 -31
  69. package/src/commands/interactive.js +21 -0
  70. package/src/commands/knowledge.js +27 -4
  71. package/src/commands/merkle-persist.js +118 -0
  72. package/src/commands/merkle.js +32 -17
  73. package/src/commands/models.js +107 -0
  74. package/src/commands/ollama.js +259 -0
  75. package/src/commands/preferences.js +102 -0
  76. package/src/commands/product.js +86 -16
  77. package/src/commands/security.js +3 -3
  78. package/src/commands/shell.js +71 -27
  79. package/src/commands/skills.js +66 -0
  80. package/src/commands/start-local.js +1 -1
  81. package/src/commands/tunnel.js +5 -4
  82. package/src/context/budget.js +58 -0
  83. package/src/context/compact.js +28 -0
  84. package/src/context/index.js +5 -0
  85. package/src/context/resolve.js +84 -0
  86. package/src/context/result-policy.js +66 -0
  87. package/src/index.js +16 -0
  88. package/src/indexing/execution-boundary.js +144 -0
  89. package/src/indexing/index.js +8 -0
  90. package/src/indexing/provider.js +41 -0
  91. package/src/knowledge/config.js +2 -2
  92. package/src/knowledge/context-pack.js +12 -1
  93. package/src/knowledge/contracts.js +9 -4
  94. package/src/knowledge/engine.js +7 -3
  95. package/src/knowledge/service/server.js +1 -1
  96. package/src/lib/auth.js +10 -19
  97. package/src/lib/bridge-client.js +7 -5
  98. package/src/lib/cli-preferences.js +74 -0
  99. package/src/lib/core-client.js +8 -8
  100. package/src/lib/deploy-receipt/index.js +5 -2
  101. package/src/lib/detect-context.js +6 -5
  102. package/src/lib/identity-scaffold.js +1 -1
  103. package/src/lib/local-scaffold.js +35 -33
  104. package/src/lib/local-status.js +9 -17
  105. package/src/lib/merkle/cloudflare-persistence.js +321 -0
  106. package/src/lib/merkle/filemeta.js +43 -0
  107. package/src/lib/merkle/git-ignore.js +24 -0
  108. package/src/lib/merkle/hash.js +1 -0
  109. package/src/lib/merkle/index.js +22 -0
  110. package/src/lib/merkle/persistence.js +72 -0
  111. package/src/lib/merkle/semantic.js +359 -0
  112. package/src/lib/merkle/snapshot.js +9 -3
  113. package/src/lib/merkle/tree.js +11 -6
  114. package/src/lib/open-url.js +66 -0
  115. package/src/lib/project-config.js +227 -0
  116. package/src/lib/project-rules.js +68 -0
  117. package/src/lib/save-sdk-token.js +1 -1
  118. package/src/lib/slash-commands.js +2 -1
  119. package/src/lib/tools.js +11 -5
  120. package/src/security/index.js +1 -0
  121. package/src/security/inventory.js +4 -1
  122. package/src/security/render.js +27 -5
  123. package/src/security/scan.js +24 -9
  124. package/src/security/trust-boundary.js +24 -0
  125. package/src/skills/index.js +64 -0
  126. package/src/tools/index.js +1 -0
  127. package/src/tools/search.js +70 -0
  128. package/src/ui/ansi.js +1 -1
  129. package/src/ui/boot.js +56 -0
  130. package/src/ui/merkle/render.js +1 -0
  131. package/src/ui/runtime-activity.js +192 -0
  132. package/src/ui/theme.js +19 -18
  133. package/test/app-building-skills.test.mjs +61 -0
  134. package/test/apps-scaffold-contract.test.mjs +56 -0
  135. package/test/capabilities.test.mjs +53 -4
  136. package/test/cli-preferences.test.mjs +25 -0
  137. package/test/context.test.mjs +95 -0
  138. package/test/indexing-provider.test.mjs +29 -0
  139. package/test/jr-dev-skill.test.mjs +26 -0
  140. package/test/knowledge-context-pack.test.mjs +19 -0
  141. package/test/knowledge.test.mjs +1 -1
  142. package/test/merkle-persistence.test.mjs +91 -0
  143. package/test/merkle.test.mjs +77 -3
  144. package/test/models.test.mjs +37 -0
  145. package/test/ollama.test.mjs +94 -0
  146. package/test/open-url.test.mjs +64 -0
  147. package/test/project-config.test.mjs +81 -0
  148. package/test/project-rules.test.mjs +44 -0
  149. package/test/release-hygiene.test.mjs +34 -0
  150. package/test/repository-snapshot-view.test.mjs +112 -0
  151. package/test/runtime-activity.test.mjs +98 -0
  152. package/test/sdk-worker-contract.test.mjs +68 -0
  153. package/test/security.test.mjs +46 -0
  154. package/test/shell.test.mjs +8 -1
  155. package/test/skills.test.mjs +22 -0
  156. package/test/smoke.mjs +2 -2
  157. package/test/theme-portability.test.mjs +14 -0
  158. package/test/tools-search.test.mjs +27 -0
  159. package/examples/agentsam-tui-ansi.mjs +0 -149
  160. package/src/commands/tui.js +0 -120
  161. package/src/ui/splash-xterm.js +0 -290
  162. package/src/ui/splash.js +0 -426
@@ -0,0 +1,44 @@
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 { defaultProjectRules, findProjectRules, loadProjectRules } from '../src/lib/project-rules.js';
7
+
8
+ test('.agentsamrules behaves like a committed repo-level instruction file', t => {
9
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-rules-'));
10
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
11
+ const nested = path.join(root, 'src', 'nested');
12
+ fs.mkdirSync(nested, { recursive: true });
13
+ const content = defaultProjectRules('demo');
14
+ fs.writeFileSync(path.join(root, '.agentsamrules'), content);
15
+
16
+ assert.equal(findProjectRules(nested), path.join(root, '.agentsamrules'));
17
+ const loaded = loadProjectRules(nested);
18
+ assert.equal(loaded.found, true);
19
+ assert.equal(loaded.content, content);
20
+ assert.match(loaded.hash, /^sha256:[a-f0-9]{64}$/);
21
+ assert.equal(loaded.truncated, false);
22
+ });
23
+
24
+ test('project rules are deterministically bounded before entering context', t => {
25
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-rules-bound-'));
26
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
27
+ fs.writeFileSync(path.join(root, '.agentsamrules'), 'x'.repeat(200));
28
+ const loaded = loadProjectRules(root, { maxChars: 40 });
29
+ assert.equal(loaded.chars, 40);
30
+ assert.equal(loaded.source_chars, 200);
31
+ assert.equal(loaded.truncated, true);
32
+ });
33
+
34
+
35
+ test('project rules discovery never leaks across the repository boundary', t => {
36
+ const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-rules-boundary-'));
37
+ t.after(() => fs.rmSync(parent, { recursive: true, force: true }));
38
+ fs.writeFileSync(path.join(parent, '.agentsamrules'), 'parent rules\n');
39
+ const repo = path.join(parent, 'repo');
40
+ const nested = path.join(repo, 'src');
41
+ fs.mkdirSync(path.join(repo, '.git'), { recursive: true });
42
+ fs.mkdirSync(nested, { recursive: true });
43
+ assert.equal(findProjectRules(nested), null);
44
+ });
@@ -0,0 +1,34 @@
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
+
6
+ const root = path.resolve(import.meta.dirname, '..');
7
+ const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
8
+
9
+ test('2.5 release metadata and public terminal vocabulary are aligned', () => {
10
+ const pkg = JSON.parse(read('package.json'));
11
+ const manifest = read('agentsam.yaml');
12
+ const readme = read('README.md');
13
+
14
+ assert.equal(pkg.version, '2.5.0');
15
+ assert.match(manifest, /target_version: "2\.5\.0"/);
16
+ assert.match(manifest, /state: release_candidate/);
17
+ assert.match(manifest, /current_latest: "2\.4\.1"/);
18
+ assert.doesNotMatch(manifest, /^\s*- tui\s*$/m);
19
+ assert.doesNotMatch(readme, /agentsam tui|CLI\/TUI/);
20
+ });
21
+
22
+ test('dead branded splash paths are gone and UI preview stays dev-only', () => {
23
+ for (const rel of ['src/ui/splash.js', 'src/ui/splash-xterm.js', 'src/commands/tui.js']) {
24
+ assert.equal(fs.existsSync(path.join(root, rel)), false, `${rel} should not ship`);
25
+ }
26
+ for (const rel of ['scripts/internal/ui-preview-runner.mjs', 'scripts/internal/ansi-ui-preview.mjs']) {
27
+ assert.equal(fs.existsSync(path.join(root, rel)), true, `${rel} should remain available to contributors`);
28
+ }
29
+ assert.equal(fs.existsSync(path.join(root, 'src/ui/runtime-activity.js')), true, 'automatic runtime activity must ship with the product CLI');
30
+
31
+ const pkg = JSON.parse(read('package.json'));
32
+ assert.ok(pkg.files.includes('skills'));
33
+ assert.equal(pkg.files.includes('scripts'), false, 'internal preview scripts must not be published');
34
+ });
@@ -0,0 +1,112 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ buildRepositorySnapshotFacets,
5
+ projectRepositorySnapshot,
6
+ repositoryFileMatches,
7
+ } from '../src/capabilities/repository-snapshot-view.js';
8
+
9
+ function snapshot() {
10
+ return {
11
+ schema_version: 1,
12
+ capability: 'repository.snapshot',
13
+ snapshot_id: 'rsnap_0123456789abcdef01234567',
14
+ created_at: '2026-09-11T00:00:00.000Z',
15
+ content_hash: `sha256:${'1'.repeat(64)}`,
16
+ repository: { full_name: 'SamPrimeaux/agentsam-sdk', branch: 'main', revision_sha: 'abc123', dirty: false },
17
+ tree: {
18
+ merkle_root: `sha256:${'2'.repeat(64)}`,
19
+ metadata_root: `sha256:${'3'.repeat(64)}`,
20
+ manifest: { format: 'agentsam-merkle', version: 1, hash_algorithm: 'sha256', policy_hash: `sha256:${'4'.repeat(64)}` },
21
+ classifier: { format: 'agentsam-filemeta', version: 1, source: 'path+package+ast' },
22
+ stats: { files: 4, directories: 3, symlinks: 0, bytes: 1000 },
23
+ semantic_stats: { entries: 4, files: 4, packages: 2, ast_indexed: 4 },
24
+ paths: ['packages/identity/src/core/accounts.js', 'packages/identity/src/oauth/login.js', 'apps/cms/src/editor.tsx', 'apps/cms/src/assets.ts'],
25
+ files: [
26
+ {
27
+ path: 'packages/identity/src/core/accounts.js', type: 'file', size: 213, mode: 420, hash: `sha256:${'a'.repeat(64)}`,
28
+ package: '@inneranimalmedia/agentsam-sdk-identity', package_root: 'packages/identity', system: 'identity',
29
+ category: 'accounts', layer: 'core', kind: 'source', language: 'javascript', role: 'business-logic', execution_domain: 'unknown',
30
+ tags: ['account-scoped', 'authentication'], symbols: ['accountLinkingNotConfigured'], imports: [],
31
+ },
32
+ {
33
+ path: 'packages/identity/src/oauth/login.js', type: 'file', size: 300, mode: 420, hash: `sha256:${'b'.repeat(64)}`,
34
+ package: '@inneranimalmedia/agentsam-sdk-identity', package_root: 'packages/identity', system: 'identity',
35
+ category: 'login', layer: 'oauth', kind: 'source', language: 'javascript', role: 'business-logic', execution_domain: 'unknown',
36
+ tags: ['authentication'], symbols: ['login'], imports: ['./accounts.js'],
37
+ },
38
+ {
39
+ path: 'apps/cms/src/editor.tsx', type: 'file', size: 250, mode: 420, hash: `sha256:${'c'.repeat(64)}`,
40
+ package: '@inneranimalmedia/cms', package_root: 'apps/cms', system: 'cms-frontend', category: 'editor', layer: 'ui',
41
+ kind: 'source', language: 'typescript', role: 'ui', execution_domain: 'browser', tags: ['cms'], symbols: ['CmsEditor'], imports: ['react'],
42
+ },
43
+ {
44
+ path: 'apps/cms/src/assets.ts', type: 'file', size: 237, mode: 420, hash: `sha256:${'d'.repeat(64)}`,
45
+ package: '@inneranimalmedia/cms', package_root: 'apps/cms', system: 'cms-frontend', category: 'assets', layer: 'data',
46
+ kind: 'source', language: 'typescript', role: 'business-logic', execution_domain: 'browser', tags: ['cms', 'assets'], symbols: ['loadAsset'], imports: [],
47
+ },
48
+ ],
49
+ },
50
+ intelligence: { summary: { file_count: 4 } },
51
+ packages: [], knowledge: { configured: false },
52
+ analysis: {
53
+ trust_boundary: {
54
+ schema_version: 1, analyzer: 'agentsam-execution-boundary', metadata_root: `sha256:${'3'.repeat(64)}`,
55
+ complete: true, status: 'action-required', ok: false, browser_roots: 2, browser_reachable_files: 2, ast_files: 4,
56
+ findings: [{ kind: 'browser-server-import', severity: 'high', source: 'apps/cms/src/editor.tsx', target: 'server/secrets.ts', repair: { action: 'move-server-call-behind-api-boundary' } }],
57
+ },
58
+ },
59
+ deploy: null,
60
+ };
61
+ }
62
+
63
+ test('repository snapshot index is bounded and preserves the three canonical identities', () => {
64
+ const full = snapshot();
65
+ const view = projectRepositorySnapshot(full, { view: 'index' });
66
+ assert.equal(view.snapshot_id, full.snapshot_id);
67
+ assert.equal(view.content_hash, full.content_hash);
68
+ assert.equal(view.tree.merkle_root, full.tree.merkle_root);
69
+ assert.equal(view.tree.metadata_root, full.tree.metadata_root);
70
+ assert.equal(view.tree.manifest.policy_hash, full.tree.manifest.policy_hash);
71
+ assert.equal(view.files, undefined);
72
+ assert.equal(view.projection.matched, 4);
73
+ assert.match(view.projection.projection_hash, /^sha256:[a-f0-9]{64}$/);
74
+ assert.equal(view.tree.paths, undefined);
75
+ assert.equal(view.tree.files, undefined);
76
+ assert.equal(view.analysis.trust_boundary.status, 'action-required');
77
+ assert.equal(view.analysis.trust_boundary.finding_count, 1);
78
+ assert.equal(view.analysis.trust_boundary.findings[0].repair_action, 'move-server-call-behind-api-boundary');
79
+ });
80
+
81
+ test('repository snapshot files view filters semantics before bounding output', () => {
82
+ const full = snapshot();
83
+ const view = projectRepositorySnapshot(full, {
84
+ view: 'files',
85
+ filters: { system: ['identity'], tag: ['authentication'], category: ['accounts'], execution_domain: ['unknown'] },
86
+ limit: 1,
87
+ });
88
+ assert.equal(view.projection.matched, 1);
89
+ assert.equal(view.projection.returned, 1);
90
+ assert.equal(view.projection.truncated, false);
91
+ assert.deepEqual(view.files.map((entry) => entry.path), ['packages/identity/src/core/accounts.js']);
92
+ assert.equal(view.files[0].hash, undefined);
93
+ assert.equal(view.files[0].mode, undefined);
94
+ });
95
+
96
+ test('path globs, AST selectors, and facet counts are deterministic', () => {
97
+ const full = snapshot();
98
+ const files = full.tree.files;
99
+ assert.equal(repositoryFileMatches(files[2], { path: ['apps/cms/**'], symbol: ['CmsEditor'] }), true);
100
+ assert.equal(repositoryFileMatches(files[2], { import: ['react'], language: ['javascript'] }), false);
101
+ const facets = buildRepositorySnapshotFacets(files);
102
+ assert.deepEqual(facets.systems.slice(0, 2), [
103
+ { value: 'cms-frontend', count: 2 },
104
+ { value: 'identity', count: 2 },
105
+ ]);
106
+ assert.deepEqual(facets.tags.find((row) => row.value === 'authentication'), { value: 'authentication', count: 2 });
107
+ assert.deepEqual(facets.execution_domains, [
108
+ { value: 'browser', count: 2 },
109
+ { value: 'unknown', count: 2 },
110
+ ]);
111
+ assert.equal(repositoryFileMatches(files[2], { execution_domain: ['browser'] }), true);
112
+ });
@@ -0,0 +1,98 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {
4
+ canAnimateTerminal,
5
+ createRuntimeActivity,
6
+ phaseForRuntimeEvent,
7
+ renderRuntimeActivityFrame,
8
+ renderRuntimeActivityPanel,
9
+ renderRuntimeActivityResult,
10
+ } from '../src/ui/runtime-activity.js';
11
+ import { runLocalAgent } from '../src/commands/shell.js';
12
+
13
+ test('runtime activity is automatic only in a real interactive terminal', () => {
14
+ assert.equal(canAnimateTerminal({ stdout: { isTTY: true }, env: { TERM: 'xterm-256color' } }), true);
15
+ assert.equal(canAnimateTerminal({ stdout: { isTTY: false }, env: { TERM: 'xterm-256color' } }), false);
16
+ assert.equal(canAnimateTerminal({ stdout: { isTTY: true }, env: { TERM: 'dumb' } }), false);
17
+ assert.equal(canAnimateTerminal({ stdout: { isTTY: true }, env: { TERM: 'xterm-256color', CI: '1' } }), false);
18
+ });
19
+
20
+ test('runtime renderer exposes thinking, tool, and context phases without product renderer vocabulary', () => {
21
+ assert.match(renderRuntimeActivityFrame({ phase: 'thinking', tick: 1, elapsedMs: 1250 }), /Agent Sam/);
22
+ assert.match(renderRuntimeActivityFrame({ phase: 'thinking', tick: 1, elapsedMs: 1250 }), /thinking/);
23
+ const panel = renderRuntimeActivityPanel({ phase: 'thinking', tick: 2, elapsedMs: 1250 });
24
+ assert.match(panel, /Agent Sam/);
25
+ assert.match(panel, /thinking · working locally/);
26
+ assert.match(panel, /\(●\)/);
27
+ assert.equal(panel.split('\n').length, 5);
28
+ assert.match(renderRuntimeActivityResult({ phase: 'done', elapsedMs: 1250 }), /done/);
29
+ assert.equal(phaseForRuntimeEvent({ type: 'model.started' }), 'thinking');
30
+ assert.equal(phaseForRuntimeEvent({ type: 'tool.started', tool: 'git.status' }), 'using git.status');
31
+ assert.equal(phaseForRuntimeEvent({ type: 'context.compaction.started' }), 'tidying context');
32
+ });
33
+
34
+ test('runtime activity redraws automatically and restores the cursor', () => {
35
+ let output = '';
36
+ let clock = 0;
37
+ let intervalFn = null;
38
+ const activity = createRuntimeActivity({
39
+ write: (text) => { output += text; },
40
+ interactive: true,
41
+ now: () => clock,
42
+ setInterval: (fn) => { intervalFn = fn; return 7; },
43
+ clearInterval: () => {},
44
+ });
45
+
46
+ activity.start('thinking');
47
+ clock = 320;
48
+ intervalFn();
49
+ activity.update('using repository.snapshot');
50
+ clock = 840;
51
+ activity.succeed('done');
52
+
53
+ assert.match(output, /thinking/);
54
+ assert.match(output, /using repository\.snapshot/);
55
+ assert.match(output, /done/);
56
+ assert.match(output, /\x1b\[\?25l/);
57
+ assert.match(output, /\x1b\[\?25h/);
58
+ });
59
+
60
+ test('/agent starts and completes product activity around the real request', async () => {
61
+ const phases = [];
62
+ let output = '';
63
+ const activity = {
64
+ start: (phase) => phases.push(`start:${phase}`),
65
+ succeed: (phase) => phases.push(`success:${phase}`),
66
+ fail: (phase) => phases.push(`fail:${phase}`),
67
+ };
68
+ const fetchImpl = async () => ({
69
+ ok: true,
70
+ status: 200,
71
+ async text() { return JSON.stringify({ ok: true, answer: 'ready' }); },
72
+ });
73
+
74
+ await runLocalAgent('inspect this repo', (text) => { output += text; }, { fetchImpl, activity, interactive: true });
75
+ assert.deepEqual(phases, ['start:thinking', 'success:done']);
76
+ assert.match(output, /"answer": "ready"/);
77
+ });
78
+
79
+
80
+ test('/agent restores product activity when the response body fails', async () => {
81
+ const phases = [];
82
+ const activity = {
83
+ start: (phase) => phases.push(`start:${phase}`),
84
+ succeed: (phase) => phases.push(`success:${phase}`),
85
+ fail: (phase) => phases.push(`fail:${phase}`),
86
+ };
87
+ const fetchImpl = async () => ({
88
+ ok: true,
89
+ status: 200,
90
+ async text() { throw new Error('body interrupted'); },
91
+ });
92
+
93
+ await assert.rejects(
94
+ runLocalAgent('inspect this repo', () => {}, { fetchImpl, activity, interactive: true }),
95
+ /response could not be read: body interrupted/,
96
+ );
97
+ assert.deepEqual(phases, ['start:thinking', 'fail:response error']);
98
+ });
@@ -0,0 +1,68 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs';
3
+ import test from 'node:test';
4
+
5
+ const read = (relative) => fs.readFileSync(new URL(`../${relative}`, import.meta.url), 'utf8');
6
+ const exists = (relative) => fs.existsSync(new URL(`../${relative}`, import.meta.url));
7
+
8
+ const wrangler = read('apps/local-studio/backend/wrangler.jsonc');
9
+ const providers = read('apps/local-studio/shared/agentsam/src/model-providers.ts');
10
+ const runtime = read('apps/local-studio/backend/server/lib/cloudflare-runtime.ts');
11
+ const worker = read('apps/local-studio/backend/worker/index.js');
12
+
13
+ test('Local Studio backend is the only agentsam-sdk Cloudflare application owner', () => {
14
+ assert.equal(exists('wrangler.jsonc'), false);
15
+ assert.equal(exists('worker/index.js'), false);
16
+ assert.equal(exists('scripts/with-cloudflare-env.sh'), false);
17
+ assert.ok(exists('apps/local-studio/backend/worker/index.js'));
18
+ assert.equal(exists('apps/local-studio/backend/wrangler.ui.toml'), false);
19
+ assert.equal(exists('apps/local-studio/backend/wrangler.workmode.toml'), false);
20
+ assert.ok(exists('apps/local-studio/backend/wrangler.jsonc'));
21
+ });
22
+
23
+ test('agentsam-sdk Worker uses the backend Worker entry, custom domain only, and canonical bindings', () => {
24
+ assert.match(wrangler, /"name"\s*:\s*"agentsam-sdk"/);
25
+ assert.match(wrangler, /"main"\s*:\s*"worker\/index\.js"/);
26
+ assert.match(wrangler, /"directory"\s*:\s*"\.\.\/\.output\/public"/);
27
+ assert.match(wrangler, /"workers_dev"\s*:\s*false/);
28
+ assert.match(wrangler, /"pattern"\s*:\s*"agentsam\.inneranimalmedia\.com"/);
29
+ assert.match(wrangler, /"binding"\s*:\s*"DB"/);
30
+ assert.match(wrangler, /"database_name"\s*:\s*"inneranimalmedia-business"/);
31
+ assert.match(wrangler, /"binding"\s*:\s*"WEBSITE_ASSETS"/);
32
+ assert.match(wrangler, /"bucket_name"\s*:\s*"agentsam-os-blueprint-content"/);
33
+ assert.match(wrangler, /"binding"\s*:\s*"AGENTSAM_WAI"/);
34
+ assert.match(wrangler, /"binding"\s*:\s*"EXECOS"/);
35
+ assert.match(wrangler, /"service"\s*:\s*"execos"/);
36
+ assert.match(wrangler, /"binding"\s*:\s*"PTY_SERVICE"/);
37
+ assert.match(wrangler, /"service_id"\s*:\s*"019db639-7c70-7071-8ef3-32ec0392a9ff"/);
38
+ assert.match(wrangler, /"IAM_ORIGIN"\s*:\s*"https:\/\/inneranimalmedia\.com"/);
39
+ assert.doesNotMatch(wrangler, /AGENTSAM_WORKER_ROLE/);
40
+ assert.doesNotMatch(wrangler, /"OLLAMA_BASE_URL"\s*:/);
41
+ assert.doesNotMatch(wrangler, /workers\.dev/);
42
+ });
43
+
44
+ test('canonical Worker preserves vault routes and delegates app traffic to Nitro', () => {
45
+ assert.match(worker, /url\.pathname\.startsWith\("\/api\/vault\/"\)/);
46
+ assert.match(worker, /\/api\/vault\/secrets/);
47
+ assert.match(worker, /\/api\/vault\/unwrap/);
48
+ assert.match(worker, /return nitroWorker\.fetch\(request, env, context\)/);
49
+ });
50
+
51
+ test('model providers include Grok independently from Grok gate authentication', () => {
52
+ for (const id of ['grok', 'openai', 'gemini', 'workers-ai', 'ollama']) {
53
+ assert.match(providers, new RegExp(`id: ["']${id}["']`));
54
+ }
55
+ assert.match(providers, /credential: "XAI_API_KEY"/);
56
+ assert.match(providers, /binding: "AGENTSAM_WAI"/);
57
+ assert.match(providers, /binding: "EXECOS"/);
58
+ });
59
+
60
+ test('Ollama uses local loopback behind ExecOS with a portable explicit cwd', () => {
61
+ assert.match(runtime, /baseUrl: "http:\/\/127\.0\.0\.1:11434"/);
62
+ assert.match(runtime, /target: "local"/);
63
+ assert.match(runtime, /x-bridge-key/);
64
+ assert.match(runtime, /return explicit \|\| "\/"/);
65
+ assert.match(runtime, /JSON\.stringify\(\{ command, target: "local", cwd \}\)/);
66
+ assert.match(runtime, /public_base_url: false/);
67
+ assert.doesNotMatch(runtime, /ollama\.inneranimalmedia\.com/);
68
+ });
@@ -181,3 +181,49 @@ test('a missing transitive dependency cannot pass as complete coverage', async t
181
181
  assert.equal(report.complete,false);
182
182
  assert.ok(report.issues.some(issue=>issue.includes('Missing locked dependency edge')));
183
183
  });
184
+
185
+ test('security scan includes AST/Merkle trust-boundary contradictions and mechanical repair evidence', async t => {
186
+ const f = fixture(t);
187
+ fs.mkdirSync(path.join(f.root, 'frontend'), { recursive: true });
188
+ fs.mkdirSync(path.join(f.root, 'backend'), { recursive: true });
189
+ fs.writeFileSync(path.join(f.root, 'frontend', 'client.ts'), [
190
+ "import { secretValue } from '../backend/secrets.ts';",
191
+ "export const value = secretValue;",
192
+ ].join('\n'));
193
+ fs.writeFileSync(path.join(f.root, 'backend', 'secrets.ts'), [
194
+ "export const secretValue = process.env.API_KEY;",
195
+ ].join('\n'));
196
+
197
+ const report = await scanProjectSecurity({ projectRoot: f.root, fetch: empty });
198
+ assert.equal(report.complete, true);
199
+ assert.equal(report.ok, false);
200
+ assert.equal(report.status, 'action-required');
201
+ assert.match(report.trust_boundary.merkle_root, /^sha256:/);
202
+ assert.match(report.trust_boundary.metadata_root, /^sha256:/);
203
+ const finding = report.trust_boundary.findings.find((item) => item.kind === 'browser-server-import');
204
+ assert.ok(finding);
205
+ assert.equal(finding.source, 'frontend/client.ts');
206
+ assert.equal(finding.target, 'backend/secrets.ts');
207
+ assert.equal(finding.repair.action, 'move-server-call-behind-api-boundary');
208
+ assert.equal(reportExitCode(report), 1);
209
+ const text = formatSecurityReport(report);
210
+ assert.match(text, /Trust boundary/);
211
+ assert.match(text, /frontend\/client\.ts/);
212
+ assert.match(text, /move-server-call-behind-api-boundary/);
213
+ });
214
+
215
+ test('browser env and server-runtime imports are derived from syntax rather than folder names alone', async t => {
216
+ const f = fixture(t);
217
+ fs.mkdirSync(path.join(f.root, 'frontend', 'src'), { recursive: true });
218
+ fs.writeFileSync(path.join(f.root, 'frontend', 'src', 'client.ts'), [
219
+ "import fs from 'node:fs';",
220
+ "export const leaked = import.meta.env.VITE_API_KEY;",
221
+ "export const privateValue = process.env.INTERNAL_TOKEN;",
222
+ "export const exists = Boolean(fs);",
223
+ ].join('\n'));
224
+ const report = await scanProjectSecurity({ projectRoot: f.root, fetch: empty });
225
+ const kinds = new Set(report.trust_boundary.findings.map((item) => item.kind));
226
+ assert.ok(kinds.has('browser-server-runtime-import'));
227
+ assert.ok(kinds.has('browser-public-secret-name'));
228
+ assert.ok(kinds.has('browser-private-env-access'));
229
+ });
@@ -4,7 +4,7 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { spawnSync } from 'node:child_process';
6
6
  import test from 'node:test';
7
- import { dispatchShellLine, renderShellCatalog, tokenizeShellLine } from '../src/commands/shell.js';
7
+ import { dispatchShellLine, renderShellCatalog, renderShellPrompt, tokenizeShellLine } from '../src/commands/shell.js';
8
8
 
9
9
  const repoRoot = path.resolve(new URL('..', import.meta.url).pathname);
10
10
 
@@ -14,6 +14,13 @@ test('shell tokenizer preserves Windows paths and quoted arguments', () => {
14
14
  assert.deepEqual(tokenizeShellLine('/agent "inspect this repo"'), ['/agent', 'inspect this repo']);
15
15
  });
16
16
 
17
+ test('interactive prompt derives username and cwd instead of hardcoding Agent Sam identity', () => {
18
+ const env = { USER: 'alice', HOME: '/Users/alice' };
19
+ assert.equal(renderShellPrompt('/Users/alice/code/demo', env), 'alice ~/code/demo > ');
20
+ assert.equal(renderShellPrompt('/Users/alice', env), 'alice ~ > ');
21
+ assert.equal(renderShellPrompt('/tmp/demo', env), 'alice /tmp/demo > ');
22
+ });
23
+
17
24
  test('shell catalog advertises commands that the REPL owns', () => {
18
25
  const catalog = renderShellCatalog();
19
26
  assert.match(catalog, /\/help\s+Show Agent Sam commands/);
@@ -0,0 +1,22 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { getSkill, listSkills, loadSkill } from '../src/skills/index.js';
4
+
5
+ test('portable skill registry exposes AgentSam Jr Dev by id and alias', () => {
6
+ const listed = listSkills();
7
+ assert.ok(listed.some((skill) => skill.id === 'agentsam-jr-dev'));
8
+
9
+ assert.equal(getSkill('agentsam_jr_dev')?.id, 'agentsam-jr-dev');
10
+ assert.equal(getSkill('jr-dev')?.id, 'agentsam-jr-dev');
11
+ });
12
+
13
+ test('AgentSam Jr Dev loads compact instructions and references on demand', () => {
14
+ const compact = loadSkill('agentsam-jr-dev');
15
+ assert.match(compact.instructions, /# AgentSam Jr Dev/);
16
+ assert.equal('references' in compact, false);
17
+
18
+ const full = loadSkill('agentsam_jr_dev', { references: true });
19
+ assert.equal(full.references.length, 2);
20
+ assert.match(full.references[0].content, /## HTTP/);
21
+ assert.match(full.references[1].content, /# Real Application Logic/);
22
+ });
package/test/smoke.mjs CHANGED
@@ -57,12 +57,12 @@ assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/db'));
57
57
  assert.ok(SLASH_COMMANDS.some((c) => c.cmd === '/agent'));
58
58
  assert.deepEqual(
59
59
  listSlashCommands({ lane: 'deploy' }).map(({ cmd }) => cmd),
60
- ['/help', '/deploy', '/exit'],
60
+ ['/help', '/models', '/settings', '/deploy', '/exit'],
61
61
  'deploy lane keeps global shell controls and excludes commands from other lanes',
62
62
  );
63
63
 
64
64
  printContextSummary({
65
- iam: { ready: true, source: 'sdk-token', detail: 'AGENTSAM_SDK_TOKEN' },
65
+ iam: { ready: true, source: 'sdk-key', detail: 'AGENTSAM_SDK_KEY' },
66
66
  gcp: { source: 'vm-metadata', email: 'execos@project.iam.gserviceaccount.com' },
67
67
  gcp_vm: true,
68
68
  github: { source: 'gh-cli', account: 'user@example.com' },
@@ -0,0 +1,14 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { CLOUD, LOCAL, SANDBOX, getLaneThemeByName } from '../src/ui/theme.js';
5
+
6
+ test('SDK terminal lane metadata is provider and operator neutral', () => {
7
+ const serialized = JSON.stringify([LOCAL.meta, CLOUD.meta, SANDBOX.meta]);
8
+ assert.doesNotMatch(serialized, /sam(smac)?|inneranimalmedia|gcp|cloudflare|MY_CONTAINER/i);
9
+ assert.equal(LOCAL.meta.tunnelHint, 'current machine · real shell');
10
+ assert.equal(CLOUD.meta.label, 'Remote');
11
+ assert.equal(SANDBOX.meta.tunnelHint, 'isolated disposable runtime');
12
+ assert.equal(getLaneThemeByName('remote'), CLOUD);
13
+ assert.equal(getLaneThemeByName('cloud'), CLOUD);
14
+ });
@@ -0,0 +1,27 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { getToolCatalog } from '../src/lib/tools.js';
4
+ import { searchToolCards } from '../src/tools/index.js';
5
+ import { DEFAULT_RESULT_POLICY } from '../src/context/index.js';
6
+
7
+ test('every built-in SDK tool inherits a bounded result policy', () => {
8
+ for (const lane of ['fullstack', 'cms', 'data', 'crm', 'creative']) {
9
+ for (const tool of getToolCatalog(lane)) assert.deepEqual(tool.result_policy, DEFAULT_RESULT_POLICY);
10
+ }
11
+ });
12
+
13
+ test('tools.search returns compact cards rather than full schemas', () => {
14
+ const catalog = Array.from({ length: 20 }, (_, index) => ({
15
+ name: index === 13 ? 'code.retrieve' : `tool.${index}`,
16
+ description: index === 13 ? 'Find symbols, callers, and semantic code matches.' : `Capability ${index}`,
17
+ category: index === 13 ? 'code' : 'misc',
18
+ risk: 'read',
19
+ input_schema: { type: 'object', required: ['query'], properties: { query: { type: 'string' }, giant: { description: 'x'.repeat(5000) } } },
20
+ }));
21
+ const result = searchToolCards(catalog, 'code symbols');
22
+ assert.ok(result.cards.length <= 8);
23
+ assert.equal(result.cards[0].tool, 'code.retrieve');
24
+ assert.deepEqual(result.cards[0].required, ['query']);
25
+ assert.equal('input_schema' in result.cards[0], false);
26
+ assert.ok(result.receipt.chars < 24_000);
27
+ });