@inneranimalmedia/agentsam-sdk 2.6.3 → 2.6.4

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 +6 -0
  2. package/README.md +16 -1
  3. package/bin/agentsam +16 -1
  4. package/docs/BRAND_INTELLIGENCE.md +1 -1
  5. package/docs/architecture/AGENTSAM_DISTRIBUTION_OWNERSHIP.md +176 -0
  6. package/docs/architecture/AGENTSAM_GO_RUNTIME.md +282 -0
  7. package/docs/architecture/CODEBASEINDEX_GUIDED_PATH.md +202 -0
  8. package/docs/architecture/FS_E2E_CLOSURE_RECEIPT.md +59 -0
  9. package/docs/architecture/LOCAL_FS_AUTHORITY.md +38 -0
  10. package/docs/architecture/LOCAL_STUDIO_CLOUDFLARE_OAUTH.md +28 -0
  11. package/docs/architecture/PLAN_CLI_AND_LOCAL_STUDIO_DESKTOP.md +96 -0
  12. package/docs/architecture/SAM_ACTIVITY_RECOVERY_RECEIPT.md +39 -0
  13. package/docs/architecture/SAM_DECISION_WORK_RECEIPT.md +54 -0
  14. package/docs/architecture/SAM_KERNEL.md +181 -0
  15. package/docs/architecture/SAM_MACHINE_NORMALIZATION_PRECOMMIT_REPORT.md +208 -0
  16. package/docs/architecture/SLASH_SKILLS_PORTABLE.md +116 -0
  17. package/docs/architecture/fs-e2e-receipt.latest.json +42 -0
  18. package/docs/architecture/previews/codebaseindex-guided-path/CODEBASEINDEX_GUIDED_PATH.md +202 -0
  19. package/docs/architecture/previews/codebaseindex-guided-path/index.html +321 -0
  20. package/migrations/d1/0010_portable_tickets_memory.sql +140 -0
  21. package/migrations/d1/0011_agentsam_skill_v2.sql +185 -0
  22. package/migrations/d1/0011b_agentsam_skill_v2_cutover.sql +22 -0
  23. package/migrations/d1/0011c_agentsam_skill_v2_backfill.sql +76 -0
  24. package/migrations/d1/0011d_agentsam_skill_v2_retrieval_revisions.sql +50 -0
  25. package/migrations/d1/0012_agentsam_tools_required_seed.sql +67 -0
  26. package/migrations/d1/0013_identity_oauth_states.sql +15 -0
  27. package/migrations/d1/0014_auth_event_log.sql +20 -0
  28. package/migrations/d1/0015_identity_oauth_state_app_id.sql +3 -0
  29. package/migrations/d1/README_PORTABLE_CONTROL_PLANE.md +16 -0
  30. package/migrations/sqlite/agentsam_skill_retrieval.portable.sql +42 -0
  31. package/package.json +19 -4
  32. package/packages/agentsam-contracts/src/errors.ts +16 -0
  33. package/packages/agentsam-errors/src/envelope.js +53 -0
  34. package/packages/agentsam-errors/src/index.js +1 -0
  35. package/packages/agentsam-errors/src/recovery.js +301 -0
  36. package/packages/agentsam-knowledge/src/providers/index.js +18 -6
  37. package/packages/connectors/cloudflare/src/routes.js +9 -0
  38. package/packages/connectors/cloudflare/tests/connector.test.mjs +26 -1
  39. package/packages/identity/.agentsam/features/oauth-login-portal/agentsam.feature.json +1 -1
  40. package/packages/identity/.agentsam/features/oauth-login-portal/routes.json +11 -2
  41. package/packages/identity/docs/PORTABLE_IDENTITY_ARCHITECTURE.md +50 -0
  42. package/packages/identity/migrations/D1_SCHEMA_MAPPING.md +31 -0
  43. package/packages/identity/migrations/sqlite/001_identity_core.sql +99 -0
  44. package/packages/identity/migrations/sqlite/002_identity_oauth_client.sql +38 -0
  45. package/packages/identity/migrations/sqlite/003_identity_oauth_server.sql +64 -0
  46. package/packages/identity/package.json +2 -2
  47. package/packages/identity/src/adapters/cloudflare-d1/index.js +122 -22
  48. package/packages/identity/src/adapters/sqlite/index.js +319 -0
  49. package/packages/identity/src/app/verify-app.js +95 -0
  50. package/packages/identity/src/contracts/identity-store.js +115 -0
  51. package/packages/identity/src/contracts/route-ids.js +30 -0
  52. package/packages/identity/src/contracts/route-projection.js +218 -0
  53. package/packages/identity/src/contracts/routes.js +11 -0
  54. package/packages/identity/src/core/browser-paths.js +4 -5
  55. package/packages/identity/src/core/constants.js +13 -8
  56. package/packages/identity/src/core/session-policy.js +32 -0
  57. package/packages/identity/src/frontend/auth-portal/pages/login.html +10 -10
  58. package/packages/identity/src/frontend/auth-portal/pages/reset.html +3 -3
  59. package/packages/identity/src/frontend/auth-portal/pages/signup.html +3 -3
  60. package/packages/identity/src/frontend/auth-portal/preview/dashboard-stub.html +1 -1
  61. package/packages/identity/src/index.js +25 -0
  62. package/packages/identity/src/oauth/README.md +10 -4
  63. package/packages/identity/src/oauth/credentials.js +20 -13
  64. package/packages/identity/src/oauth/finalize-inbound.js +1 -1
  65. package/packages/identity/src/oauth/iam-platform.js +8 -7
  66. package/packages/identity/src/oauth/redirect-paths.js +27 -36
  67. package/packages/identity/src/server/identity-service.js +30 -13
  68. package/packages/identity/src/server/mount-policy.js +30 -0
  69. package/packages/identity/src/server/post-auth.js +79 -0
  70. package/packages/identity/src/server/worker-router.js +104 -72
  71. package/packages/identity/tests/finalize-inbound-oauth.test.mjs +6 -6
  72. package/packages/identity/tests/iam-provider.test.mjs +1 -1
  73. package/packages/identity/tests/identity-service.test.mjs +36 -5
  74. package/packages/identity/tests/oauth-credentials.test.mjs +3 -1
  75. package/packages/identity/tests/portable-identity-architecture.test.mjs +157 -0
  76. package/packages/identity/tests/session-routes-policy.test.mjs +21 -0
  77. package/packages/theme-church-site/package.json +2 -1
  78. package/packages/theme-church-site/src/index.js +1 -0
  79. package/packages/theme-companions-site/package.json +2 -1
  80. package/packages/theme-companions-site/src/index.js +1 -0
  81. package/packages/theme-floors-site/package.json +2 -1
  82. package/packages/theme-floors-site/src/index.js +1 -0
  83. package/packages/theme-fuelnfree-site/package.json +2 -1
  84. package/packages/theme-fuelnfree-site/src/index.js +1 -0
  85. package/packages/theme-handyman-site/package.json +2 -1
  86. package/packages/theme-handyman-site/src/index.js +1 -0
  87. package/packages/theme-insurance-site/package.json +2 -1
  88. package/packages/theme-insurance-site/src/index.js +1 -0
  89. package/packages/theme-shinshu-site/package.json +2 -1
  90. package/packages/theme-shinshu-site/src/index.js +1 -0
  91. package/protocol/apps/agentsam.app.v1.schema.json +51 -0
  92. package/protocol/brand/brandpack.v1.schema.json +43 -0
  93. package/protocol/credentials/issue.v1.schema.json +38 -0
  94. package/protocol/database/connection.v1.schema.json +41 -0
  95. package/protocol/embeddings/embedding-profile.v1.schema.json +20 -0
  96. package/protocol/errors/error-envelope.schema.json +135 -1
  97. package/protocol/errors/recovery.v1.schema.json +50 -0
  98. package/protocol/runtime/workspace-fs.v1.schema.json +71 -0
  99. package/protocol/sam/activity.v1.schema.json +48 -0
  100. package/protocol/sam/answer.v1.schema.json +35 -0
  101. package/protocol/sam/calibration.v1.schema.json +21 -0
  102. package/protocol/sam/decision-receipt.v1.schema.json +29 -0
  103. package/protocol/sam/evaluation.v1.schema.json +19 -0
  104. package/protocol/sam/operation.schema.json +66 -0
  105. package/protocol/sam/outcome.v1.schema.json +36 -0
  106. package/protocol/sam/question.v1.schema.json +26 -0
  107. package/protocol/sam/registry.seed.json +153 -0
  108. package/protocol/sam/result.schema.json +53 -0
  109. package/protocol/sam/state.v1.schema.json +23 -0
  110. package/protocol/skills/agentsam.interaction.v1.schema.json +50 -0
  111. package/protocol/skills/agentsam.skill.v1.schema.json +57 -0
  112. package/protocol/ui/icon-registry.mjs +226 -0
  113. package/protocol/ui/icon.v1.schema.json +51 -0
  114. package/skills/README.md +22 -9
  115. package/skills/agentsam-codebaseindex/SKILL.md +225 -0
  116. package/skills/catalog.json +14 -0
  117. package/src/cli/command-catalog.js +130 -0
  118. package/src/cli/dispatch.js +48 -0
  119. package/src/cli.js +43 -1
  120. package/src/commands/api-key.js +244 -0
  121. package/src/commands/app.js +60 -28
  122. package/src/commands/brand.js +17 -19
  123. package/src/commands/codebaseindex.js +688 -0
  124. package/src/commands/env.js +152 -25
  125. package/src/commands/go.js +366 -53
  126. package/src/commands/interaction-clack.js +115 -0
  127. package/src/commands/models.js +1 -1
  128. package/src/commands/providers.js +62 -14
  129. package/src/commands/shell.js +43 -2
  130. package/src/commands/skill.js +248 -0
  131. package/src/commands/skills.js +1 -1
  132. package/src/commands/start-local.js +4 -0
  133. package/src/commands/whoami.js +90 -18
  134. package/src/go/build.js +229 -39
  135. package/src/go/cloudflare.js +506 -105
  136. package/src/go/container.js +120 -0
  137. package/src/go/contract.js +9 -4
  138. package/src/go/discover.js +149 -33
  139. package/src/go/index.js +15 -3
  140. package/src/go/native-probe-runner.mjs +119 -0
  141. package/src/go/{registry.js → official-registry.js} +64 -7
  142. package/src/go/official-release.js +10 -0
  143. package/src/go/receipts.js +77 -10
  144. package/src/go/verify.js +9 -2
  145. package/src/index.js +27 -0
  146. package/src/indexing/ingest/discover-models.js +298 -0
  147. package/src/indexing/ingest/inventory.js +243 -0
  148. package/src/indexing/ingest/job-graph.js +181 -0
  149. package/src/indexing/ingest/materials.js +210 -0
  150. package/src/lib/provider-credentials.js +63 -21
  151. package/src/lib/slash-commands.js +1 -0
  152. package/src/local-fs/capability.js +121 -0
  153. package/src/local-fs/freshness.js +75 -0
  154. package/src/local-fs/index.js +385 -0
  155. package/src/local-fs/paths.js +100 -0
  156. package/src/local-pty/server.js +295 -19
  157. package/src/mcp/client.js +2 -2
  158. package/src/models/ai-access-onboarding.js +112 -0
  159. package/src/models/discovery.js +10 -2
  160. package/src/models/inventory-core.js +9 -1
  161. package/src/sam/activity/index.js +183 -0
  162. package/src/sam/client.js +252 -0
  163. package/src/sam/decision/calibration.js +109 -0
  164. package/src/sam/decision/confidence.js +126 -0
  165. package/src/sam/decision/evaluate.js +157 -0
  166. package/src/sam/decision/evaluators/deterministic.js +341 -0
  167. package/src/sam/decision/evaluators/heuristic.js +61 -0
  168. package/src/sam/decision/evaluators/select.js +50 -0
  169. package/src/sam/decision/evaluators/semantic.js +149 -0
  170. package/src/sam/decision/hierarchical.js +61 -0
  171. package/src/sam/decision/index.js +53 -0
  172. package/src/sam/decision/policy.js +86 -0
  173. package/src/sam/decision/questions.js +120 -0
  174. package/src/sam/decision/receipt.js +148 -0
  175. package/src/sam/decision/state.js +117 -0
  176. package/src/sam/decision/types.js +22 -0
  177. package/src/sam/decision/validate.js +200 -0
  178. package/src/sam/define.js +51 -0
  179. package/src/sam/index.js +65 -0
  180. package/src/sam/operations/brand-scan.js +62 -0
  181. package/src/sam/operations/cad-blender-inspect.js +36 -0
  182. package/src/sam/operations/codebaseindex-ingest.js +49 -0
  183. package/src/sam/operations/decision-evaluate.js +59 -0
  184. package/src/sam/operations/planning-astar.js +77 -0
  185. package/src/sam/operations/planning-goap.js +60 -0
  186. package/src/sam/operations/repository-inspect.js +72 -0
  187. package/src/sam/operations/security-scan.js +33 -0
  188. package/src/sam/operations/terminal-exec.js +29 -0
  189. package/src/sam/planning/astar.js +311 -0
  190. package/src/sam/planning/goap.js +177 -0
  191. package/src/sam/planning/index.js +21 -0
  192. package/src/sam/planning/state.js +84 -0
  193. package/src/sam/registry.js +48 -0
  194. package/src/sam/result.js +77 -0
  195. package/src/sam/seed.js +44 -0
  196. package/src/sam/types.js +91 -0
  197. package/src/skills/catalog.js +64 -0
  198. package/src/skills/content-resolver.js +124 -0
  199. package/src/skills/hosted-store.js +37 -0
  200. package/src/skills/index.js +29 -64
  201. package/src/skills/interaction.js +102 -0
  202. package/src/skills/local-store.js +228 -0
  203. package/src/skills/manifest.js +104 -0
  204. package/src/skills/metrics.js +31 -0
  205. package/src/skills/registry.js +184 -0
  206. package/src/skills/runtime.js +287 -0
  207. package/src/skills/slash.js +44 -0
  208. package/src/ui/cli/help.js +94 -101
  209. package/test/cli/api-key-env-whoami.test.mjs +129 -0
  210. package/test/cli/codebaseindex-plan-ux.test.mjs +37 -0
  211. package/test/cli/go.test.mjs +62 -5
  212. package/test/cli/skill-npm-and-env.test.mjs +52 -0
  213. package/test/cli/wireframes-go-registry.test.mjs +87 -2
  214. package/test/go/build-source-identity.test.mjs +31 -0
  215. package/test/go/cloudflare-probe.test.mjs +274 -10
  216. package/test/go/distribution.test.mjs +28 -0
  217. package/test/integration/ai-access-onboarding.test.mjs +47 -0
  218. package/test/integration/cli-help.test.mjs +1 -1
  219. package/test/integration/cms-site-tenancy-contract.test.mjs +6 -6
  220. package/test/integration/codebaseindex-ingest.test.mjs +166 -0
  221. package/test/integration/icon-registry.test.mjs +67 -0
  222. package/test/integration/ingest-discover-models.test.mjs +30 -0
  223. package/test/integration/install-script.test.mjs +12 -9
  224. package/test/integration/local-fs.test.mjs +113 -0
  225. package/test/integration/provider-env-cli.test.mjs +2 -1
  226. package/test/integration/sam-activity-recovery.test.mjs +147 -0
  227. package/test/integration/sam-decision.test.mjs +584 -0
  228. package/test/integration/sam-kernel.test.mjs +99 -0
  229. package/test/integration/sam-planning-astar.test.mjs +279 -0
  230. package/test/integration/skill-runtime.test.mjs +240 -0
  231. package/test/integration/studio-fs-pty-e2e.test.mjs +294 -0
  232. package/test/models.test.mjs +14 -7
  233. package/test/shell.test.mjs +4 -4
@@ -129,28 +129,17 @@ export function agentEnvLoaderPath(options = {}) {
129
129
 
130
130
  export function ensureAgentEnvLoader(options = {}) {
131
131
  const filename = agentEnvLoaderPath(options);
132
- const supported = Object.keys(PROVIDER_CREDENTIALS).join('|');
133
- const source = `# AgentSam provider environment loader. Source this file; do not execute it.
134
- if [ "$#" -eq 0 ]; then
135
- echo "usage: source ~/.agentsam/load-agent-env.sh <profile> [profile ...]" >&2
136
- return 2 2>/dev/null || exit 2
132
+ const source = `# AgentSam environment loader. Source this file; do not execute it.
133
+ # Credentials resolve from the OS keychain / local vault via \`agentsam env shell\`.
134
+ # Do not put plaintext secrets in this file.
135
+ if command -v agentsam >/dev/null 2>&1; then
136
+ eval "$(agentsam env shell --profile default)"
137
+ elif command -v agentsam-env >/dev/null 2>&1; then
138
+ eval "$(agentsam-env shell --profile default)"
139
+ else
140
+ echo "agentsam CLI not found on PATH; cannot load AgentSam credentials" >&2
141
+ return 1 2>/dev/null || exit 1
137
142
  fi
138
- for _agentsam_profile in "$@"; do
139
- case "$_agentsam_profile" in
140
- grok) _agentsam_profile="xai" ;;
141
- ${supported}) ;;
142
- *) echo "unknown AgentSam provider profile: $_agentsam_profile" >&2; return 2 2>/dev/null || exit 2 ;;
143
- esac
144
- _agentsam_file="\${HOME}/.agentsam/env.d/\${_agentsam_profile}.env"
145
- if [ ! -f "$_agentsam_file" ]; then
146
- echo "AgentSam provider profile not found: $_agentsam_file" >&2
147
- return 1 2>/dev/null || exit 1
148
- fi
149
- set -a
150
- . "$_agentsam_file"
151
- set +a
152
- done
153
- unset _agentsam_file _agentsam_profile
154
143
  `;
155
144
  atomicWrite(filename, source, 0o700);
156
145
  return filename;
@@ -359,3 +348,56 @@ export function describeProviderCredential(provider, options = {}) {
359
348
  export function listProviderCredentialStatus(options = {}) {
360
349
  return Object.freeze(Object.keys(PROVIDER_CREDENTIALS).map((provider) => describeProviderCredential(provider, options)));
361
350
  }
351
+
352
+ /**
353
+ * Emit shell `export` lines for configured credentials from vault/keychain/env.
354
+ * Bootstrap-safe: does not require AGENTSAM_API_KEY already present to resolve it.
355
+ * Prefer vault/OS store over ambient env when `preferVault` is true (default for shell).
356
+ */
357
+ export function renderEnvShellExports(options = {}) {
358
+ const profile = clean(options.profile || 'default').toLowerCase();
359
+ const wanted = Array.isArray(options.providers) && options.providers.length
360
+ ? options.providers.map(normalizeProviderId).filter(Boolean)
361
+ : profile === 'default'
362
+ ? Object.keys(PROVIDER_CREDENTIALS)
363
+ : profile.split(/[,\s]+/).map(normalizeProviderId).filter(Boolean);
364
+
365
+ const lines = ['# AgentSam env shell — evaluated by: eval "$(agentsam env shell)"'];
366
+ const emitted = [];
367
+
368
+ for (const provider of wanted) {
369
+ const spec = providerCredentialSpec(provider);
370
+ if (!spec) continue;
371
+
372
+ let resolved;
373
+ if (options.preferVault !== false) {
374
+ // Skip ambient process.env so a stale empty shell does not block keychain.
375
+ const vaultOnly = { ...options, env: { ...(options.env || {}), [spec.env]: undefined } };
376
+ if (spec.accountEnv) {
377
+ for (const name of spec.accountEnv) vaultOnly.env[name] = undefined;
378
+ }
379
+ resolved = resolveProviderCredential(spec.provider, vaultOnly);
380
+ if (!resolved.configured) {
381
+ resolved = resolveProviderCredential(spec.provider, options);
382
+ }
383
+ } else {
384
+ resolved = resolveProviderCredential(spec.provider, options);
385
+ }
386
+
387
+ if (!resolved.configured || !resolved.value) continue;
388
+ lines.push(`export ${spec.env}=${envLiteral(resolved.value)}`);
389
+ if (spec.provider === 'cloudflare' && resolved.account_id) {
390
+ lines.push(`export CLOUDFLARE_ACCOUNT_ID=${envLiteral(resolved.account_id)}`);
391
+ }
392
+ emitted.push(spec.provider);
393
+ }
394
+
395
+ if (!emitted.length) {
396
+ lines.push('# No AgentSam credentials configured. Run: agentsam api-key create --store keychain --activate');
397
+ }
398
+ return Object.freeze({
399
+ profile,
400
+ providers: emitted,
401
+ script: `${lines.join('\n')}\n`,
402
+ });
403
+ }
@@ -35,6 +35,7 @@ export const SLASH_COMMANDS = [
35
35
  { cmd: '/logs', description: 'Show local Agent Sam execution events', lane: 'observability' },
36
36
  { cmd: '/deploy', description: 'Add a cloud adapter and deploy intentionally', lane: 'deploy' },
37
37
  { cmd: '/clear', description: 'Clear the terminal display', lane: 'terminal' },
38
+ { cmd: '/skills', description: 'List resolvable slash skill vocabulary', lane: 'agent' },
38
39
  { cmd: '/help', description: 'Show the factual implemented command catalog' },
39
40
  { cmd: '/exit', description: 'Exit Agent Sam shell and return to the host terminal' },
40
41
  ];
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Local runtime workspace capability — short-lived FS/PTY authority for one root.
3
+ *
4
+ * Compatible with later:
5
+ * account → terminal connection → enrolled instance → approved workspace root
6
+ *
7
+ * Local today:
8
+ * start-local binds one root + mints one capability.
9
+ * Browser cannot invent an absolute root to gain authority.
10
+ */
11
+
12
+ import crypto from 'node:crypto';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+
16
+ export const WORKSPACE_CAPABILITY_HEADER = 'x-agentsam-workspace-capability';
17
+ export const LOCAL_RUNTIME_FILE = 'local-runtime.json';
18
+
19
+ /**
20
+ * @param {{ root: string, port: number, host?: string }} opts
21
+ */
22
+ export function mintWorkspaceCapability(opts) {
23
+ const root = path.resolve(opts.root);
24
+ const workspace_id = `ws_${crypto.randomBytes(10).toString('hex')}`;
25
+ const capability = `awc_${crypto.randomBytes(24).toString('hex')}`;
26
+ const expires_at = new Date(Date.now() + 12 * 60 * 60 * 1000).toISOString();
27
+ return Object.freeze({
28
+ schema: 'agentsam.local-workspace-capability.v1',
29
+ workspace_id,
30
+ capability,
31
+ root,
32
+ host: opts.host || '127.0.0.1',
33
+ port: opts.port,
34
+ runtimeBaseUrl: `http://${opts.host || '127.0.0.1'}:${opts.port}`,
35
+ fsBaseUrl: `http://${opts.host || '127.0.0.1'}:${opts.port}/v1/fs`,
36
+ expires_at,
37
+ purpose: 'local.filesystem+pty',
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Persist capability beside the authorized root (machine-local; not file content authority).
43
+ * @param {object} record
44
+ */
45
+ export function writeLocalRuntimeRecord(record) {
46
+ const dir = path.join(record.root, '.agentsam');
47
+ fs.mkdirSync(dir, { recursive: true });
48
+ const file = path.join(dir, LOCAL_RUNTIME_FILE);
49
+ fs.writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
50
+ return file;
51
+ }
52
+
53
+ /**
54
+ * @param {string} root
55
+ */
56
+ export function readLocalRuntimeRecord(root) {
57
+ const file = path.join(path.resolve(root), '.agentsam', LOCAL_RUNTIME_FILE);
58
+ if (!fs.existsSync(file)) return null;
59
+ try {
60
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * @param {import('http').IncomingMessage} req
68
+ * @param {string} expected
69
+ */
70
+ export function extractCapability(req, expected) {
71
+ const header = req.headers[WORKSPACE_CAPABILITY_HEADER]
72
+ || req.headers.authorization
73
+ || '';
74
+ let token = '';
75
+ if (typeof header === 'string') {
76
+ if (header.toLowerCase().startsWith('bearer ')) token = header.slice(7).trim();
77
+ else token = header.trim();
78
+ }
79
+ if (!token && req.url) {
80
+ try {
81
+ const u = new URL(req.url, 'http://127.0.0.1');
82
+ token = u.searchParams.get('capability') || '';
83
+ } catch {
84
+ /* ignore */
85
+ }
86
+ }
87
+ return {
88
+ ok: Boolean(expected && token && token === expected),
89
+ token,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Loopback + Studio origin allowlist. Never `*`.
95
+ * @param {string|undefined} origin
96
+ * @param {string[]} [extra]
97
+ */
98
+ export function isAllowedStudioOrigin(origin, extra = []) {
99
+ if (!origin || origin === 'null') return true; // non-browser / same-origin tooling
100
+ const allow = [
101
+ 'http://127.0.0.1:8080',
102
+ 'http://localhost:8080',
103
+ 'http://127.0.0.1:5173',
104
+ 'http://localhost:5173',
105
+ 'http://127.0.0.1:3000',
106
+ 'http://localhost:3000',
107
+ ...extra,
108
+ ];
109
+ return allow.includes(origin);
110
+ }
111
+
112
+ /**
113
+ * @param {import('http').IncomingMessage} req
114
+ */
115
+ export function isLoopbackRemote(req) {
116
+ const raw = req.socket?.remoteAddress || '';
117
+ return raw === '127.0.0.1'
118
+ || raw === '::1'
119
+ || raw === '::ffff:127.0.0.1'
120
+ || raw.endsWith('127.0.0.1');
121
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Minimal repository/index freshness bridge.
3
+ * Filesystem writes mark generation stale — do not re-index on every keystroke.
4
+ */
5
+
6
+ /** @type {Map<string, { generation: number, stale_paths: Set<string>, updated_at: string }>} */
7
+ const roots = new Map();
8
+
9
+ /**
10
+ * @param {string} root
11
+ * @param {{ paths?: string[], reason?: string }} [opts]
12
+ */
13
+ export function markRepositoryStale(root, opts = {}) {
14
+ const key = String(root);
15
+ const prev = roots.get(key) || {
16
+ generation: 0,
17
+ stale_paths: new Set(),
18
+ updated_at: null,
19
+ };
20
+ const nextGen = prev.generation + 1;
21
+ const stale_paths = new Set(prev.stale_paths);
22
+ for (const p of opts.paths || []) stale_paths.add(String(p));
23
+ const record = {
24
+ generation: nextGen,
25
+ stale_paths,
26
+ updated_at: new Date().toISOString(),
27
+ reason: opts.reason || 'filesystem_write',
28
+ };
29
+ roots.set(key, record);
30
+ return {
31
+ ok: true,
32
+ schema: 'agentsam.repository-freshness.v1',
33
+ root: key,
34
+ generation: record.generation,
35
+ stale_paths: [...stale_paths].slice(0, 200),
36
+ updated_at: record.updated_at,
37
+ reason: record.reason,
38
+ };
39
+ }
40
+
41
+ /**
42
+ * @param {string} root
43
+ */
44
+ export function getRepositoryFreshness(root) {
45
+ const key = String(root);
46
+ const record = roots.get(key);
47
+ if (!record) {
48
+ return {
49
+ ok: true,
50
+ schema: 'agentsam.repository-freshness.v1',
51
+ root: key,
52
+ generation: 0,
53
+ stale: false,
54
+ stale_paths: [],
55
+ };
56
+ }
57
+ return {
58
+ ok: true,
59
+ schema: 'agentsam.repository-freshness.v1',
60
+ root: key,
61
+ generation: record.generation,
62
+ stale: record.stale_paths.size > 0 || record.generation > 0,
63
+ stale_paths: [...record.stale_paths].slice(0, 200),
64
+ updated_at: record.updated_at,
65
+ reason: record.reason,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * @param {string} root
71
+ */
72
+ export function clearRepositoryFreshness(root) {
73
+ roots.delete(String(root));
74
+ return { ok: true };
75
+ }
@@ -0,0 +1,385 @@
1
+ /**
2
+ * Local machine filesystem operations under an authorized workspace root.
3
+ * Version = sha256 of file bytes (content-addressed optimistic concurrency).
4
+ */
5
+ import crypto from 'node:crypto';
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { resolveContainedPath } from './paths.js';
10
+ import { markRepositoryStale } from './freshness.js';
11
+
12
+ export const LOCAL_FS_ENGINE = 'agentsam-local-fs-v1';
13
+ export const MAX_TEXT_BYTES = 2 * 1024 * 1024; // 2 MiB text default
14
+
15
+ /**
16
+ * @param {string} root
17
+ */
18
+ export function createLocalFilesystem(root) {
19
+ const resolvedRoot = path.resolve(root);
20
+
21
+ function contain(rel) {
22
+ return resolveContainedPath(resolvedRoot, rel);
23
+ }
24
+
25
+ function hashBytes(buf) {
26
+ return `sha256:${crypto.createHash('sha256').update(buf).digest('hex')}`;
27
+ }
28
+
29
+ /**
30
+ * @param {string} relPath
31
+ * @param {{ recursive?: boolean, maxDepth?: number }} [opts]
32
+ */
33
+ function list(relPath = '.', opts = {}) {
34
+ const gate = contain(relPath);
35
+ if (!gate.ok) return gate;
36
+ const recursive = Boolean(opts.recursive);
37
+ const maxDepth = opts.maxDepth ?? 6;
38
+ /** @type {Array<{ path: string, kind: string, size: number, mtime: number }>} */
39
+ const items = [];
40
+ const skip = new Set(['.git', 'node_modules', '.output', 'dist', '.DS_Store']);
41
+
42
+ /**
43
+ * @param {string} abs
44
+ * @param {string} rel
45
+ * @param {number} depth
46
+ */
47
+ function walk(abs, rel, depth) {
48
+ let entries;
49
+ try {
50
+ entries = fs.readdirSync(abs, { withFileTypes: true });
51
+ } catch (err) {
52
+ return {
53
+ ok: false,
54
+ error: err instanceof Error ? err.message : String(err),
55
+ code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'list_failed',
56
+ };
57
+ }
58
+ for (const e of entries) {
59
+ if (skip.has(e.name) || e.name === '.DS_Store') continue;
60
+ const childRel = rel === '.' ? e.name : `${rel}/${e.name}`;
61
+ const childAbs = path.join(abs, e.name);
62
+ let size = 0;
63
+ let mtime = 0;
64
+ try {
65
+ const st = fs.lstatSync(childAbs);
66
+ size = st.size;
67
+ mtime = Math.floor(st.mtimeMs);
68
+ } catch {
69
+ /* ignore */
70
+ }
71
+ const kind = e.isDirectory() ? 'directory' : e.isSymbolicLink() ? 'symlink' : 'file';
72
+ items.push({ path: childRel, kind, size, mtime });
73
+ if (recursive && kind === 'directory' && depth < maxDepth) {
74
+ const nested = walk(childAbs, childRel, depth + 1);
75
+ if (nested && nested.ok === false) return nested;
76
+ }
77
+ }
78
+ return { ok: true };
79
+ }
80
+
81
+ const walked = walk(gate.abs, gate.rel, 0);
82
+ if (walked && walked.ok === false) return walked;
83
+
84
+ items.sort((a, b) => {
85
+ if (a.kind !== b.kind) return a.kind === 'directory' ? -1 : 1;
86
+ return a.path.localeCompare(b.path);
87
+ });
88
+ return { ok: true, root: resolvedRoot, path: gate.rel, entries: items, engine: LOCAL_FS_ENGINE };
89
+ }
90
+
91
+ /**
92
+ * @param {string} relPath
93
+ */
94
+ function stat(relPath) {
95
+ const gate = contain(relPath);
96
+ if (!gate.ok) return gate;
97
+ try {
98
+ const st = fs.lstatSync(gate.abs);
99
+ const kind = st.isDirectory() ? 'directory' : st.isSymbolicLink() ? 'symlink' : 'file';
100
+ let content_hash = null;
101
+ if (kind === 'file' && st.size <= MAX_TEXT_BYTES) {
102
+ content_hash = hashBytes(fs.readFileSync(gate.abs));
103
+ }
104
+ return {
105
+ ok: true,
106
+ path: gate.rel,
107
+ kind,
108
+ size: st.size,
109
+ mtime: Math.floor(st.mtimeMs),
110
+ content_hash,
111
+ binary: kind === 'file' && st.size > 0 ? null : false,
112
+ engine: LOCAL_FS_ENGINE,
113
+ };
114
+ } catch (err) {
115
+ return {
116
+ ok: false,
117
+ error: err instanceof Error ? err.message : String(err),
118
+ code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'stat_failed',
119
+ };
120
+ }
121
+ }
122
+
123
+ /**
124
+ * @param {string} relPath
125
+ * @param {{ maxBytes?: number }} [opts]
126
+ */
127
+ function read(relPath, opts = {}) {
128
+ const gate = contain(relPath);
129
+ if (!gate.ok) return gate;
130
+ const maxBytes = opts.maxBytes ?? MAX_TEXT_BYTES;
131
+ try {
132
+ const st = fs.statSync(gate.abs);
133
+ if (st.isDirectory()) {
134
+ return { ok: false, error: 'is_directory', code: 'is_directory' };
135
+ }
136
+ if (st.size > maxBytes) {
137
+ return {
138
+ ok: false,
139
+ error: `file_too_large:${st.size}>${maxBytes}`,
140
+ code: 'file_too_large',
141
+ size: st.size,
142
+ };
143
+ }
144
+ const buf = fs.readFileSync(gate.abs);
145
+ // Detect obvious binary
146
+ const sample = buf.subarray(0, Math.min(buf.length, 8000));
147
+ if (sample.includes(0)) {
148
+ return {
149
+ ok: false,
150
+ error: 'binary_file',
151
+ code: 'binary_file',
152
+ size: st.size,
153
+ content_hash: hashBytes(buf),
154
+ };
155
+ }
156
+ const content = buf.toString('utf8');
157
+ const version = hashBytes(buf);
158
+ return {
159
+ ok: true,
160
+ path: gate.rel,
161
+ content,
162
+ version,
163
+ content_hash: version,
164
+ mtime: Math.floor(st.mtimeMs),
165
+ size: st.size,
166
+ engine: LOCAL_FS_ENGINE,
167
+ };
168
+ } catch (err) {
169
+ return {
170
+ ok: false,
171
+ error: err instanceof Error ? err.message : String(err),
172
+ code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'read_failed',
173
+ };
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Atomic write with optional optimistic concurrency (expectedVersion).
179
+ * @param {string} relPath
180
+ * @param {string} content
181
+ * @param {{ expectedVersion?: string|null, overwrite?: boolean }} [opts]
182
+ */
183
+ function write(relPath, content, opts = {}) {
184
+ const gate = contain(relPath);
185
+ if (!gate.ok) return gate;
186
+ if (typeof content !== 'string') {
187
+ return { ok: false, error: 'content_must_be_string', code: 'invalid_input' };
188
+ }
189
+ const buf = Buffer.from(content, 'utf8');
190
+ if (buf.length > MAX_TEXT_BYTES) {
191
+ return { ok: false, error: 'content_too_large', code: 'file_too_large' };
192
+ }
193
+
194
+ const exists = fs.existsSync(gate.abs);
195
+ if (exists) {
196
+ const current = fs.readFileSync(gate.abs);
197
+ const currentVersion = hashBytes(current);
198
+ if (opts.expectedVersion != null && opts.expectedVersion !== '' && opts.expectedVersion !== currentVersion) {
199
+ if (!opts.overwrite) {
200
+ return {
201
+ ok: false,
202
+ error: 'version_conflict',
203
+ code: 'version_conflict',
204
+ expected: opts.expectedVersion,
205
+ actual: currentVersion,
206
+ path: gate.rel,
207
+ };
208
+ }
209
+ }
210
+ } else if (opts.expectedVersion) {
211
+ return {
212
+ ok: false,
213
+ error: 'version_conflict',
214
+ code: 'version_conflict',
215
+ expected: opts.expectedVersion,
216
+ actual: null,
217
+ path: gate.rel,
218
+ };
219
+ }
220
+
221
+ try {
222
+ fs.mkdirSync(path.dirname(gate.abs), { recursive: true });
223
+ const tmp = path.join(
224
+ path.dirname(gate.abs),
225
+ `.agentsam-write-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`,
226
+ );
227
+ fs.writeFileSync(tmp, buf);
228
+ fs.renameSync(tmp, gate.abs);
229
+ const version = hashBytes(buf);
230
+ const st = fs.statSync(gate.abs);
231
+ markRepositoryStale(resolvedRoot, { paths: [gate.rel], reason: 'write' });
232
+ return {
233
+ ok: true,
234
+ path: gate.rel,
235
+ version,
236
+ content_hash: version,
237
+ mtime: Math.floor(st.mtimeMs),
238
+ size: st.size,
239
+ engine: LOCAL_FS_ENGINE,
240
+ };
241
+ } catch (err) {
242
+ return {
243
+ ok: false,
244
+ error: err instanceof Error ? err.message : String(err),
245
+ code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'write_failed',
246
+ };
247
+ }
248
+ }
249
+
250
+ /**
251
+ * @param {string} relPath
252
+ * @param {string} [content]
253
+ */
254
+ function create(relPath, content = '') {
255
+ const gate = contain(relPath);
256
+ if (!gate.ok) return gate;
257
+ if (fs.existsSync(gate.abs)) {
258
+ return { ok: false, error: 'already_exists', code: 'already_exists', path: gate.rel };
259
+ }
260
+ return write(relPath, content, {});
261
+ }
262
+
263
+ /**
264
+ * @param {string} fromRel
265
+ * @param {string} toRel
266
+ * @param {{ expectedVersion?: string|null }} [opts]
267
+ */
268
+ function rename(fromRel, toRel, opts = {}) {
269
+ const from = contain(fromRel);
270
+ if (!from.ok) return from;
271
+ const to = contain(toRel);
272
+ if (!to.ok) return to;
273
+ if (!fs.existsSync(from.abs)) {
274
+ return { ok: false, error: 'not_found', code: 'ENOENT', path: from.rel };
275
+ }
276
+ if (opts.expectedVersion) {
277
+ const st = fs.statSync(from.abs);
278
+ if (st.isFile()) {
279
+ const current = hashBytes(fs.readFileSync(from.abs));
280
+ if (current !== opts.expectedVersion) {
281
+ return {
282
+ ok: false,
283
+ error: 'version_conflict',
284
+ code: 'version_conflict',
285
+ expected: opts.expectedVersion,
286
+ actual: current,
287
+ };
288
+ }
289
+ }
290
+ }
291
+ if (fs.existsSync(to.abs)) {
292
+ return { ok: false, error: 'target_exists', code: 'already_exists', path: to.rel };
293
+ }
294
+ try {
295
+ fs.mkdirSync(path.dirname(to.abs), { recursive: true });
296
+ fs.renameSync(from.abs, to.abs);
297
+ markRepositoryStale(resolvedRoot, { paths: [from.rel, to.rel], reason: 'rename' });
298
+ return { ok: true, from: from.rel, to: to.rel, engine: LOCAL_FS_ENGINE };
299
+ } catch (err) {
300
+ return {
301
+ ok: false,
302
+ error: err instanceof Error ? err.message : String(err),
303
+ code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'rename_failed',
304
+ };
305
+ }
306
+ }
307
+
308
+ /**
309
+ * @param {string} relPath
310
+ * @param {{ expectedVersion?: string|null, recursive?: boolean }} [opts]
311
+ */
312
+ function remove(relPath, opts = {}) {
313
+ const gate = contain(relPath);
314
+ if (!gate.ok) return gate;
315
+ if (!fs.existsSync(gate.abs)) {
316
+ return { ok: false, error: 'not_found', code: 'ENOENT', path: gate.rel };
317
+ }
318
+ const st = fs.lstatSync(gate.abs);
319
+ if (st.isFile() && opts.expectedVersion) {
320
+ const current = hashBytes(fs.readFileSync(gate.abs));
321
+ if (current !== opts.expectedVersion) {
322
+ return {
323
+ ok: false,
324
+ error: 'version_conflict',
325
+ code: 'version_conflict',
326
+ expected: opts.expectedVersion,
327
+ actual: current,
328
+ };
329
+ }
330
+ }
331
+ try {
332
+ if (st.isDirectory()) {
333
+ fs.rmSync(gate.abs, { recursive: Boolean(opts.recursive), force: false });
334
+ } else {
335
+ fs.unlinkSync(gate.abs);
336
+ }
337
+ markRepositoryStale(resolvedRoot, { paths: [gate.rel], reason: 'remove' });
338
+ return { ok: true, path: gate.rel, engine: LOCAL_FS_ENGINE };
339
+ } catch (err) {
340
+ return {
341
+ ok: false,
342
+ error: err instanceof Error ? err.message : String(err),
343
+ code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'remove_failed',
344
+ };
345
+ }
346
+ }
347
+
348
+ /**
349
+ * @param {string} relPath
350
+ */
351
+ function mkdir(relPath) {
352
+ const gate = contain(relPath);
353
+ if (!gate.ok) return gate;
354
+ try {
355
+ fs.mkdirSync(gate.abs, { recursive: true });
356
+ return { ok: true, path: gate.rel, engine: LOCAL_FS_ENGINE };
357
+ } catch (err) {
358
+ return {
359
+ ok: false,
360
+ error: err instanceof Error ? err.message : String(err),
361
+ code: err && typeof err === 'object' && 'code' in err ? String(err.code) : 'mkdir_failed',
362
+ };
363
+ }
364
+ }
365
+
366
+ return {
367
+ root: resolvedRoot,
368
+ engine: LOCAL_FS_ENGINE,
369
+ list,
370
+ stat,
371
+ read,
372
+ write,
373
+ create,
374
+ rename,
375
+ remove,
376
+ mkdir,
377
+ };
378
+ }
379
+
380
+ /**
381
+ * Disposable scratch root under os.tmpdir for tests.
382
+ */
383
+ export function createTempWorkspaceRoot(prefix = 'agentsam-fs-') {
384
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
385
+ }