@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,227 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { tryResolveGitContext } from './git-context.js';
5
+
6
+ export const PROJECT_CONFIG_PATH = '.agentsam/config.json';
7
+ export const PROJECT_CONFIG_SCHEMA_VERSION = 2;
8
+
9
+ function providerForHost(host) {
10
+ const value = String(host || '').toLowerCase();
11
+ if (value === 'github.com') return 'github';
12
+ if (value === 'gitlab.com') return 'gitlab';
13
+ if (value === 'bitbucket.org') return 'bitbucket';
14
+ return value ? 'git' : null;
15
+ }
16
+
17
+ function clean(value) {
18
+ return value == null ? '' : String(value).trim();
19
+ }
20
+
21
+ export function portableRepositoryIdFromGit(cwd = process.cwd()) {
22
+ const git = tryResolveGitContext({ cwd });
23
+ if (!git) return null;
24
+ const provider = providerForHost(git.remoteHost);
25
+ if (!provider || !git.repoFullName) return null;
26
+ const fullName = provider === 'github' ? git.repoFullName.toLowerCase() : git.repoFullName;
27
+ return `${provider}:${fullName}`;
28
+ }
29
+
30
+ export function newLocalRepositoryId() {
31
+ return `local:${randomUUID()}`;
32
+ }
33
+
34
+ export function createProjectManifest({
35
+ projectName,
36
+ preset = null,
37
+ profile = 'default',
38
+ runTarget = 'local',
39
+ sdkVersion = null,
40
+ repositoryId = null,
41
+ repositoryRemote = 'origin',
42
+ } = {}) {
43
+ const name = clean(projectName);
44
+ if (!name) throw new Error('project_name_required');
45
+ const repoId = clean(repositoryId) || newLocalRepositoryId();
46
+ const target = clean(runTarget) || 'local';
47
+ return {
48
+ schema_version: PROJECT_CONFIG_SCHEMA_VERSION,
49
+ project: {
50
+ name,
51
+ },
52
+ repository: {
53
+ id: repoId,
54
+ remote: clean(repositoryRemote) || 'origin',
55
+ },
56
+ product: {
57
+ preset: preset ? clean(preset) : null,
58
+ features: [],
59
+ capabilities: [],
60
+ },
61
+ defaults: {
62
+ mode: 'agent',
63
+ profile: clean(profile) || 'default',
64
+ runtime: 'local',
65
+ model: 'auto',
66
+ deploy_target: target === 'local' ? null : target,
67
+ },
68
+ merkle: {
69
+ enabled: true,
70
+ semantic: true,
71
+ persistence: 'auto',
72
+ },
73
+ rules: {
74
+ file: '.agentsamrules',
75
+ },
76
+ knowledge: {
77
+ config: '.agentsam/knowledge.json',
78
+ },
79
+ local: {
80
+ database: '.agentsam/data/agentsam.sqlite',
81
+ schema: 'db/schema.sql',
82
+ },
83
+ sdk: {
84
+ created_with: sdkVersion || null,
85
+ },
86
+ };
87
+ }
88
+
89
+ export function projectConfigPath(root) {
90
+ return path.join(path.resolve(root), PROJECT_CONFIG_PATH);
91
+ }
92
+
93
+ export function readProjectConfig(root) {
94
+ return JSON.parse(fs.readFileSync(projectConfigPath(root), 'utf8'));
95
+ }
96
+
97
+ export function tryReadProjectConfig(root) {
98
+ try {
99
+ return readProjectConfig(root);
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ export function writeProjectConfig(root, config) {
106
+ const filename = projectConfigPath(root);
107
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
108
+ fs.writeFileSync(filename, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
109
+ return filename;
110
+ }
111
+
112
+ export function isProjectManifestV2(config) {
113
+ return Number(config?.schema_version) === PROJECT_CONFIG_SCHEMA_VERSION && Boolean(config?.project?.name);
114
+ }
115
+
116
+ // Accessors deliberately understand the old flat scaffold so existing projects keep working
117
+ // while new projects use the committed portable v2 manifest.
118
+ export function getProjectName(config, fallback = '') {
119
+ return clean(config?.project?.name || config?.project || fallback);
120
+ }
121
+
122
+ export function getProjectPreset(config) {
123
+ return clean(config?.product?.preset || config?.preset || config?.lane) || null;
124
+ }
125
+
126
+ export function getDefaultProfile(config) {
127
+ return clean(config?.defaults?.profile || config?.agent) || 'default';
128
+ }
129
+
130
+ export function getDefaultRuntime(config) {
131
+ return clean(config?.defaults?.runtime || config?.run_target) || 'local';
132
+ }
133
+
134
+ export function getDefaultModel(config) {
135
+ return clean(config?.defaults?.model) || 'auto';
136
+ }
137
+
138
+ export function getDeployTarget(config) {
139
+ return clean(config?.defaults?.deploy_target || config?.deploy_target) || null;
140
+ }
141
+
142
+ export function getRepositoryId(config) {
143
+ return clean(config?.repository?.id) || null;
144
+ }
145
+
146
+ export function getProjectRulesPath(config) {
147
+ return clean(config?.rules?.file) || '.agentsamrules';
148
+ }
149
+
150
+ export function getKnowledgeConfigPath(config) {
151
+ return clean(config?.knowledge?.config) || '.agentsam/knowledge.json';
152
+ }
153
+
154
+ export function getLocalDatabasePath(config) {
155
+ return clean(config?.local?.database || config?.db_path) || '.agentsam/data/agentsam.sqlite';
156
+ }
157
+
158
+ export function getLocalSchemaPath(config) {
159
+ return clean(config?.local?.schema || config?.db_schema) || 'db/schema.sql';
160
+ }
161
+
162
+ export function getCreatedWithVersion(config) {
163
+ return clean(config?.sdk?.created_with || config?.scaffold_version) || null;
164
+ }
165
+
166
+ export function setDeployTarget(config, target) {
167
+ const value = clean(target) || null;
168
+ if (isProjectManifestV2(config)) {
169
+ config.defaults ||= {};
170
+ config.defaults.deploy_target = value;
171
+ } else {
172
+ config.deploy_target = value;
173
+ }
174
+ return config;
175
+ }
176
+
177
+ export function setProductPreset(config, preset) {
178
+ if (isProjectManifestV2(config)) {
179
+ config.product ||= { preset: null, features: [], capabilities: [] };
180
+ config.product.preset = clean(preset?.id) || null;
181
+ config.product.features = [...(preset?.features || [])];
182
+ config.product.capabilities = [...(preset?.capabilities || [])];
183
+ } else {
184
+ config.preset = clean(preset?.id) || null;
185
+ config.features = [...(preset?.features || [])];
186
+ config.capabilities = [...(preset?.capabilities || [])];
187
+ }
188
+ return config;
189
+ }
190
+
191
+ export function setLocalModelCapability(config, provider = 'ollama') {
192
+ if (isProjectManifestV2(config)) {
193
+ config.models ||= { default: getDefaultModel(config) };
194
+ config.models.local = {
195
+ provider,
196
+ base_url_env: 'OLLAMA_BASE_URL',
197
+ model_env: 'OLLAMA_MODEL',
198
+ embed_model_env: 'OLLAMA_EMBED_MODEL',
199
+ };
200
+ } else {
201
+ config.local_model = {
202
+ provider,
203
+ local_only: true,
204
+ base_url_env: 'OLLAMA_BASE_URL',
205
+ model_env: 'OLLAMA_MODEL',
206
+ embed_model_env: 'OLLAMA_EMBED_MODEL',
207
+ };
208
+ }
209
+ return config;
210
+ }
211
+
212
+ export function ensureProjectManifest(root, options = {}) {
213
+ const existing = tryReadProjectConfig(root);
214
+ if (existing) return existing;
215
+ const gitId = portableRepositoryIdFromGit(root);
216
+ const manifest = createProjectManifest({
217
+ projectName: options.projectName || path.basename(path.resolve(root)),
218
+ preset: options.preset ?? null,
219
+ profile: options.profile || 'default',
220
+ runTarget: options.runTarget || 'local',
221
+ sdkVersion: options.sdkVersion || null,
222
+ repositoryId: options.repositoryId || gitId || newLocalRepositoryId(),
223
+ repositoryRemote: options.repositoryRemote || 'origin',
224
+ });
225
+ writeProjectConfig(root, manifest);
226
+ return manifest;
227
+ }
@@ -0,0 +1,68 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+
5
+ export const PROJECT_RULES_FILENAME = '.agentsamrules';
6
+ export const DEFAULT_PROJECT_RULES_MAX_CHARS = 24_000;
7
+
8
+ function sha256(value) {
9
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
10
+ }
11
+
12
+ export function defaultProjectRules(projectName = 'this project') {
13
+ return `# Agent Sam project rules
14
+
15
+ These instructions are committed with ${projectName} and apply when Agent Sam works in this repository.
16
+
17
+ - Prefer repository evidence over assumptions.
18
+ - Prefer structural lookup before semantic retrieval when an exact symbol or relationship can answer the question.
19
+ - Keep retrieval bounded: metadata -> cards -> excerpt -> range -> full object only when required.
20
+ - Keep secrets, account/runtime state, active runs, and machine-local preferences out of committed project configuration.
21
+ - Treat Git as source-control authority and Merkle snapshots as filesystem evidence.
22
+ - Run the smallest relevant validation before reporting a code change as complete.
23
+
24
+ ## Project-specific rules
25
+
26
+ Add repository-specific conventions, commands, constraints, or architecture notes below this line.
27
+ `;
28
+ }
29
+
30
+ export function ensureProjectRules(root, projectName = path.basename(path.resolve(root))) {
31
+ const filename = path.join(path.resolve(root), PROJECT_RULES_FILENAME);
32
+ if (fs.existsSync(filename)) return filename;
33
+ fs.writeFileSync(filename, defaultProjectRules(projectName), { encoding: 'utf8', flag: 'wx' });
34
+ return filename;
35
+ }
36
+
37
+ export function findProjectRules(startDir = process.cwd()) {
38
+ let dir = path.resolve(startDir);
39
+ for (let i = 0; i < 16; i += 1) {
40
+ const filename = path.join(dir, PROJECT_RULES_FILENAME);
41
+ if (fs.existsSync(filename)) return filename;
42
+ const projectBoundary = fs.existsSync(path.join(dir, '.git')) || fs.existsSync(path.join(dir, '.agentsam', 'config.json'));
43
+ if (projectBoundary) break;
44
+ const parent = path.dirname(dir);
45
+ if (parent === dir) break;
46
+ dir = parent;
47
+ }
48
+ return null;
49
+ }
50
+
51
+ export function loadProjectRules(startDir = process.cwd(), options = {}) {
52
+ const maxChars = Number(options.maxChars ?? DEFAULT_PROJECT_RULES_MAX_CHARS);
53
+ if (!Number.isInteger(maxChars) || maxChars < 1) throw new RangeError('project rules maxChars must be a positive integer');
54
+ const filename = options.filename ? path.resolve(options.filename) : findProjectRules(startDir);
55
+ if (!filename) return Object.freeze({ found: false, path: null, content: '', chars: 0, truncated: false, hash: null });
56
+ const source = fs.readFileSync(filename, 'utf8');
57
+ const truncated = source.length > maxChars;
58
+ const content = truncated ? `${source.slice(0, Math.max(0, maxChars - 1))}…` : source;
59
+ return Object.freeze({
60
+ found: true,
61
+ path: filename,
62
+ content,
63
+ chars: content.length,
64
+ source_chars: source.length,
65
+ truncated,
66
+ hash: sha256(source),
67
+ });
68
+ }
@@ -11,7 +11,7 @@ export function saveSdkTokenHint(cwd, token) {
11
11
  fs.mkdirSync(dir, { recursive: true });
12
12
  const hintPath = path.join(dir, 'credentials.env.example');
13
13
  const content = `# Agent Sam SDK — add to your shell profile (optional)
14
- export AGENTSAM_SDK_TOKEN=${t}
14
+ export AGENTSAM_SDK_KEY=${t}
15
15
  export USER_GCP_PROJECT=YOUR_GCP_PROJECT_ID
16
16
  `;
17
17
  fs.writeFileSync(hintPath, content, 'utf8');
@@ -15,8 +15,9 @@ export const SLASH_COMMANDS = [
15
15
  { cmd: '/git', description: 'Git status, diff, branch, commit, and remote', lane: 'git' },
16
16
  { cmd: '/db', description: 'Local SQLite status and query helpers', lane: 'data' },
17
17
  { cmd: '/agent', description: 'Send a goal to the configured Agent Sam', lane: 'agent' },
18
+ { cmd: '/models', description: 'Show available model providers and local models' },
19
+ { cmd: '/settings', description: 'Choose project, runtime, terminal, and model preference' },
18
20
  { cmd: '/logs', description: 'Show local Agent Sam execution events', lane: 'observability' },
19
- { cmd: '/tui', description: 'Switch or preview terminal presentation', lane: 'terminal' },
20
21
  { cmd: '/deploy', description: 'Add a cloud adapter and deploy intentionally', lane: 'deploy' },
21
22
  { cmd: '/exit', description: 'Exit Agent Sam shell and return to the host terminal' },
22
23
  ];
package/src/lib/tools.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { DEFAULT_RESULT_POLICY } from '../context/result-policy.js';
2
+
1
3
  const CATALOG = {
2
4
  fullstack: [
3
5
  { name: 'plan', description: 'Break a goal into implementation steps.' },
@@ -30,9 +32,13 @@ export function normalizeLane(lane = 'fullstack') {
30
32
 
31
33
  export function getToolCatalog(lane = 'fullstack') {
32
34
  const key = normalizeLane(lane);
33
- if (key.includes('cms')) return CATALOG.cms;
34
- if (key.includes('data')) return CATALOG.data;
35
- if (key.includes('customer') || key.includes('crm')) return CATALOG.crm;
36
- if (key.includes('creative') || key.includes('design')) return CATALOG.creative;
37
- return CATALOG.fullstack;
35
+ const rows = key.includes('cms') ? CATALOG.cms
36
+ : key.includes('data') ? CATALOG.data
37
+ : (key.includes('customer') || key.includes('crm')) ? CATALOG.crm
38
+ : (key.includes('creative') || key.includes('design')) ? CATALOG.creative
39
+ : CATALOG.fullstack;
40
+ return rows.map((tool) => ({
41
+ ...tool,
42
+ result_policy: { ...DEFAULT_RESULT_POLICY },
43
+ }));
38
44
  }
@@ -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,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;
@@ -0,0 +1 @@
1
+ export { searchToolCards, toToolCard } from './search.js';
@@ -0,0 +1,70 @@
1
+ import { DEFAULT_RESULT_POLICY, normalizeResultPolicy } from '../context/result-policy.js';
2
+
3
+ function text(value) { return value == null ? '' : String(value).trim(); }
4
+
5
+ export function toToolCard(tool = {}) {
6
+ const required = Array.isArray(tool.required)
7
+ ? tool.required
8
+ : Array.isArray(tool.input_schema?.required)
9
+ ? tool.input_schema.required
10
+ : [];
11
+ return Object.freeze({
12
+ tool: text(tool.tool || tool.name),
13
+ summary: text(tool.summary || tool.description),
14
+ category: text(tool.category) || undefined,
15
+ risk: text(tool.risk) || 'read',
16
+ required: Object.freeze([...required]),
17
+ result_class: text(tool.result_class) || 'bounded_evidence',
18
+ });
19
+ }
20
+
21
+ function score(card, query) {
22
+ if (!query) return 1;
23
+ const q = query.toLowerCase();
24
+ const terms = q.split(/\s+/).filter(Boolean);
25
+ const haystack = `${card.tool} ${card.summary} ${card.category || ''}`.toLowerCase();
26
+ let total = 0;
27
+ if (card.tool.toLowerCase() === q) total += 100;
28
+ if (card.tool.toLowerCase().includes(q)) total += 30;
29
+ if (card.summary.toLowerCase().includes(q)) total += 15;
30
+ for (const term of terms) {
31
+ if (card.tool.toLowerCase().includes(term)) total += 8;
32
+ if (card.summary.toLowerCase().includes(term)) total += 4;
33
+ if ((card.category || '').toLowerCase().includes(term)) total += 2;
34
+ }
35
+ return total;
36
+ }
37
+
38
+ export function searchToolCards(catalog = [], query = '', options = {}) {
39
+ if (!Array.isArray(catalog)) throw new TypeError('catalog must be an array');
40
+ const policy = normalizeResultPolicy({
41
+ max_items: options.maxItems ?? DEFAULT_RESULT_POLICY.max_items,
42
+ max_chars: options.maxChars ?? DEFAULT_RESULT_POLICY.max_chars,
43
+ detail: 'card',
44
+ }, { ceiling: DEFAULT_RESULT_POLICY });
45
+ const cards = catalog.map(toToolCard)
46
+ .map((card, index) => ({ card, score: score(card, text(query)), index }))
47
+ .filter((row) => row.card.tool && row.score > 0)
48
+ .sort((a, b) => (b.score - a.score) || (a.index - b.index))
49
+ .slice(0, policy.max_items)
50
+ .map((row) => row.card);
51
+
52
+ let chars = 0;
53
+ const bounded = [];
54
+ for (const card of cards) {
55
+ const size = JSON.stringify(card).length;
56
+ if (chars + size > policy.max_chars) break;
57
+ bounded.push(card);
58
+ chars += size;
59
+ }
60
+ return Object.freeze({
61
+ query: text(query),
62
+ cards: Object.freeze(bounded),
63
+ receipt: Object.freeze({
64
+ catalog_items: catalog.length,
65
+ returned_items: bounded.length,
66
+ chars,
67
+ result_policy: policy,
68
+ }),
69
+ });
70
+ }
package/src/ui/ansi.js CHANGED
@@ -40,7 +40,7 @@ export function renderLocalStatus(status) {
40
40
  return lines.join('\n');
41
41
  }
42
42
 
43
- lines.push(row('lane', `${status.lane} · ${status.agent}`, width));
43
+ lines.push(row('profile', `${status.lane || 'custom'} · ${status.agent || 'default'}`, width));
44
44
  lines.push(
45
45
  row(
46
46
  'git',