@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
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Path containment for local machine filesystem authority.
3
+ * Rejects NUL, traversal, and symlink escapes outside the approved root.
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+
8
+ /**
9
+ * @param {string} root
10
+ * @param {string} relativeOrAbs
11
+ * @returns {{ ok: true, abs: string, rel: string } | { ok: false, error: string, code: string }}
12
+ */
13
+ export function resolveContainedPath(root, relativeOrAbs) {
14
+ if (typeof root !== 'string' || !root.trim()) {
15
+ return { ok: false, error: 'workspace_root_required', code: 'invalid_root' };
16
+ }
17
+ if (typeof relativeOrAbs !== 'string') {
18
+ return { ok: false, error: 'path_required', code: 'invalid_path' };
19
+ }
20
+ if (relativeOrAbs.includes('\0')) {
21
+ return { ok: false, error: 'nul_in_path', code: 'invalid_path' };
22
+ }
23
+
24
+ let rootReal;
25
+ try {
26
+ rootReal = fs.realpathSync(path.resolve(root));
27
+ } catch (err) {
28
+ return {
29
+ ok: false,
30
+ error: err instanceof Error ? err.message : String(err),
31
+ code: 'root_unresolvable',
32
+ };
33
+ }
34
+
35
+ const joined = path.isAbsolute(relativeOrAbs)
36
+ ? path.normalize(relativeOrAbs)
37
+ : path.resolve(rootReal, relativeOrAbs);
38
+
39
+ // Reject obvious escapes before realpath (missing parents still checked via prefix).
40
+ const relToRoot = path.relative(rootReal, joined);
41
+ if (relToRoot.startsWith('..') || path.isAbsolute(relToRoot)) {
42
+ return { ok: false, error: 'path_escape', code: 'path_escape' };
43
+ }
44
+
45
+ let absReal = joined;
46
+ try {
47
+ absReal = fs.realpathSync(joined);
48
+ } catch (err) {
49
+ // File may not exist yet (create/write) — ensure parent is inside root.
50
+ if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {
51
+ const parent = path.dirname(joined);
52
+ let parentReal;
53
+ try {
54
+ parentReal = fs.realpathSync(parent);
55
+ } catch (parentErr) {
56
+ // Walk up until an existing ancestor within root.
57
+ let cursor = parent;
58
+ let found = null;
59
+ while (cursor.startsWith(rootReal)) {
60
+ if (fs.existsSync(cursor)) {
61
+ try {
62
+ found = fs.realpathSync(cursor);
63
+ break;
64
+ } catch {
65
+ break;
66
+ }
67
+ }
68
+ const next = path.dirname(cursor);
69
+ if (next === cursor) break;
70
+ cursor = next;
71
+ }
72
+ if (!found || (found !== rootReal && !found.startsWith(rootReal + path.sep))) {
73
+ return {
74
+ ok: false,
75
+ error: parentErr instanceof Error ? parentErr.message : String(parentErr),
76
+ code: 'path_escape',
77
+ };
78
+ }
79
+ parentReal = found;
80
+ }
81
+ if (parentReal !== rootReal && !parentReal.startsWith(rootReal + path.sep)) {
82
+ return { ok: false, error: 'path_escape', code: 'path_escape' };
83
+ }
84
+ absReal = joined;
85
+ } else {
86
+ return {
87
+ ok: false,
88
+ error: err instanceof Error ? err.message : String(err),
89
+ code: 'path_unresolvable',
90
+ };
91
+ }
92
+ }
93
+
94
+ if (absReal !== rootReal && !absReal.startsWith(rootReal + path.sep)) {
95
+ return { ok: false, error: 'symlink_escape', code: 'path_escape' };
96
+ }
97
+
98
+ const rel = absReal === rootReal ? '.' : path.relative(rootReal, absReal).split(path.sep).join('/');
99
+ return { ok: true, abs: absReal, rel: rel || '.' };
100
+ }
@@ -1,12 +1,23 @@
1
1
  /**
2
- * Agent Sam local PTY — localhost WebSocket shell, no tunnel, no IAM.
3
- * Compatible with iam-pty wire format (raw bytes + JSON resize/slash).
2
+ * Agent Sam local PTY + filesystem — localhost WebSocket shell + /v1/fs HTTP.
3
+ * No tunnel, no IAM. Compatible with iam-pty wire format (raw bytes + JSON resize/slash).
4
4
  */
5
5
  import fs from 'node:fs';
6
6
  import http from 'node:http';
7
7
  import path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { WebSocketServer } from 'ws';
10
+ import { createLocalFilesystem } from '../local-fs/index.js';
11
+ import { resolveContainedPath } from '../local-fs/paths.js';
12
+ import { getRepositoryFreshness } from '../local-fs/freshness.js';
13
+ import {
14
+ mintWorkspaceCapability,
15
+ writeLocalRuntimeRecord,
16
+ extractCapability,
17
+ isAllowedStudioOrigin,
18
+ isLoopbackRemote,
19
+ WORKSPACE_CAPABILITY_HEADER,
20
+ } from '../local-fs/capability.js';
10
21
 
11
22
  const DEFAULT_PORT = 3099;
12
23
 
@@ -20,6 +31,49 @@ function shellForPlatform() {
20
31
  return process.env.SHELL || '/bin/zsh';
21
32
  }
22
33
 
34
+ function readJsonBody(req) {
35
+ return new Promise((resolve, reject) => {
36
+ const chunks = [];
37
+ req.on('data', (c) => chunks.push(c));
38
+ req.on('end', () => {
39
+ const raw = Buffer.concat(chunks).toString('utf8');
40
+ if (!raw) return resolve({});
41
+ try {
42
+ resolve(JSON.parse(raw));
43
+ } catch (err) {
44
+ reject(err);
45
+ }
46
+ });
47
+ req.on('error', reject);
48
+ });
49
+ }
50
+
51
+ /**
52
+ * @param {import('http').IncomingMessage} req
53
+ * @param {import('http').ServerResponse} res
54
+ * @param {number} status
55
+ * @param {object} body
56
+ * @param {{ allowOrigin?: string|null }} [opts]
57
+ */
58
+ function sendJson(req, res, status, body, opts = {}) {
59
+ const origin = typeof req.headers.origin === 'string' ? req.headers.origin : '';
60
+ const allowOrigin = opts.allowOrigin !== undefined
61
+ ? opts.allowOrigin
62
+ : (isAllowedStudioOrigin(origin) ? (origin || null) : null);
63
+ /** @type {Record<string, string>} */
64
+ const headers = {
65
+ 'Content-Type': 'application/json; charset=utf-8',
66
+ 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
67
+ 'Access-Control-Allow-Headers': `Content-Type, Authorization, ${WORKSPACE_CAPABILITY_HEADER}`,
68
+ };
69
+ if (allowOrigin) {
70
+ headers['Access-Control-Allow-Origin'] = allowOrigin;
71
+ headers.Vary = 'Origin';
72
+ }
73
+ res.writeHead(status, headers);
74
+ res.end(JSON.stringify(body));
75
+ }
76
+
23
77
  export function ensureNodePtySpawnHelperExecutable(options = {}) {
24
78
  const platform = options.platform || process.platform;
25
79
  const arch = options.arch || process.arch;
@@ -130,26 +184,165 @@ export function attachLocalPtySession({ ws, pty, shell, cwd, cols = 80, rows = 2
130
184
  */
131
185
  export async function startLocalPtyServer(opts = {}) {
132
186
  const pty = await loadPty(opts.pty);
133
- const cwd = opts.cwd || process.cwd();
187
+ const cwd = path.resolve(opts.cwd || process.cwd());
134
188
  const requestedPort = opts.port === 0 ? 0 : parsePort(opts.port ?? process.env.PTY_PORT, DEFAULT_PORT);
135
189
  const host = opts.host || '127.0.0.1';
136
- const shell = shellForPlatform();
137
-
138
- const httpServer = http.createServer((req, res) => {
139
- const path = (req.url || '/').split('?')[0];
140
- if (path === '/health') {
141
- res.writeHead(200, { 'Content-Type': 'application/json' });
142
- res.end(
143
- JSON.stringify({
144
- ok: true,
145
- service: 'agentsam-local-pty',
146
- cwd,
147
- port: Number(httpServer.address()?.port || requestedPort),
148
- shell,
149
- }),
150
- );
190
+ if (host !== '127.0.0.1' && host !== 'localhost' && host !== '::1') {
191
+ throw new Error('local PTY/FS must bind loopback only (127.0.0.1)');
192
+ }
193
+ const shell = opts.shell || shellForPlatform();
194
+ const filesystem = createLocalFilesystem(cwd);
195
+ // Capability minted after bind so port is known; provisional id first.
196
+ /** @type {ReturnType<typeof mintWorkspaceCapability>|null} */
197
+ let capability = null;
198
+
199
+ const httpServer = http.createServer(async (req, res) => {
200
+ const url = new URL(req.url || '/', `http://${host}`);
201
+ const pathname = url.pathname;
202
+ const origin = typeof req.headers.origin === 'string' ? req.headers.origin : '';
203
+
204
+ if (req.method === 'OPTIONS') {
205
+ if (!isAllowedStudioOrigin(origin)) {
206
+ res.writeHead(403);
207
+ res.end();
208
+ return;
209
+ }
210
+ res.writeHead(204, {
211
+ 'Access-Control-Allow-Origin': origin || 'http://127.0.0.1:8080',
212
+ 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
213
+ 'Access-Control-Allow-Headers': `Content-Type, Authorization, ${WORKSPACE_CAPABILITY_HEADER}`,
214
+ Vary: 'Origin',
215
+ });
216
+ res.end();
217
+ return;
218
+ }
219
+
220
+ if (pathname === '/health') {
221
+ sendJson(req, res, 200, {
222
+ ok: true,
223
+ service: 'agentsam-local-pty',
224
+ cwd,
225
+ root: cwd,
226
+ port: Number(httpServer.address()?.port || requestedPort),
227
+ shell,
228
+ filesystem: true,
229
+ filesystem_engine: filesystem.engine,
230
+ workspace_id: capability?.workspace_id || null,
231
+ capability_required: true,
232
+ binding: host,
233
+ });
151
234
  return;
152
235
  }
236
+
237
+ // Bootstrap: loopback-only claim of the authorized workspace capability.
238
+ // Does NOT accept an arbitrary root — root is always the process-bound cwd.
239
+ if (pathname === '/v1/workspace/bootstrap' && req.method === 'GET') {
240
+ if (!isLoopbackRemote(req)) {
241
+ return sendJson(req, res, 403, {
242
+ ok: false,
243
+ error: 'loopback_required',
244
+ code: 'WORKSPACE_BOOTSTRAP_DENIED',
245
+ });
246
+ }
247
+ if (origin && !isAllowedStudioOrigin(origin)) {
248
+ return sendJson(req, res, 403, {
249
+ ok: false,
250
+ error: 'origin_not_allowed',
251
+ code: 'WORKSPACE_BOOTSTRAP_DENIED',
252
+ }, { allowOrigin: null });
253
+ }
254
+ const requestedRoot = url.searchParams.get('root');
255
+ if (requestedRoot) {
256
+ const resolved = path.resolve(requestedRoot);
257
+ if (resolved !== cwd) {
258
+ return sendJson(req, res, 403, {
259
+ ok: false,
260
+ error: 'root_not_authorized',
261
+ code: 'WORKSPACE_ROOT_MISMATCH',
262
+ message: 'Cannot claim an arbitrary path. Start the runtime from the desired workspace root.',
263
+ authorized_root: cwd,
264
+ requested_root: resolved,
265
+ });
266
+ }
267
+ }
268
+ return sendJson(req, res, 200, {
269
+ ok: true,
270
+ ...capability,
271
+ freshness: getRepositoryFreshness(cwd),
272
+ });
273
+ }
274
+
275
+ if (pathname === '/v1/freshness' && req.method === 'GET') {
276
+ const gate = requireCapability(req, res, capability);
277
+ if (!gate) return;
278
+ return sendJson(req, res, 200, getRepositoryFreshness(cwd));
279
+ }
280
+
281
+ if (pathname === '/v1/fs' || pathname === '/v1/fs/') {
282
+ const gate = requireCapability(req, res, capability);
283
+ if (!gate) return;
284
+ return sendJson(req, res, 200, {
285
+ ok: true,
286
+ root: filesystem.root,
287
+ workspace_id: capability.workspace_id,
288
+ engine: filesystem.engine,
289
+ endpoints: ['list', 'stat', 'read', 'write', 'create', 'rename', 'remove', 'mkdir'],
290
+ });
291
+ }
292
+
293
+ if (pathname.startsWith('/v1/fs/')) {
294
+ const gate = requireCapability(req, res, capability);
295
+ if (!gate) return;
296
+ try {
297
+ const action = pathname.slice('/v1/fs/'.length).replace(/\/$/, '');
298
+ const qPath = url.searchParams.get('path') || '.';
299
+ if (req.method === 'GET' && action === 'list') {
300
+ const recursive = url.searchParams.get('recursive') === '1' || url.searchParams.get('recursive') === 'true';
301
+ return sendJson(req, res, 200, filesystem.list(qPath, { recursive, maxDepth: 5 }));
302
+ }
303
+ if (req.method === 'GET' && action === 'stat') {
304
+ return sendJson(req, res, 200, filesystem.stat(qPath));
305
+ }
306
+ if (req.method === 'GET' && action === 'read') {
307
+ return sendJson(req, res, 200, filesystem.read(qPath));
308
+ }
309
+ if (req.method === 'POST' || req.method === 'PUT' || req.method === 'DELETE') {
310
+ const body = await readJsonBody(req);
311
+ if (action === 'write') {
312
+ return sendJson(req, res, 200, filesystem.write(body.path || qPath, body.content ?? '', {
313
+ expectedVersion: body.expectedVersion ?? body.expected_version,
314
+ overwrite: Boolean(body.overwrite),
315
+ }));
316
+ }
317
+ if (action === 'create') {
318
+ return sendJson(req, res, 200, filesystem.create(body.path || qPath, body.content ?? ''));
319
+ }
320
+ if (action === 'rename') {
321
+ return sendJson(req, res, 200, filesystem.rename(body.from || qPath, body.to, {
322
+ expectedVersion: body.expectedVersion ?? body.expected_version,
323
+ }));
324
+ }
325
+ if (action === 'remove' || action === 'delete') {
326
+ return sendJson(req, res, 200, filesystem.remove(body.path || qPath, {
327
+ expectedVersion: body.expectedVersion ?? body.expected_version,
328
+ recursive: Boolean(body.recursive),
329
+ }));
330
+ }
331
+ if (action === 'mkdir') {
332
+ return sendJson(req, res, 200, filesystem.mkdir(body.path || qPath));
333
+ }
334
+ }
335
+ sendJson(req, res, 404, { ok: false, error: 'unknown_fs_action', action });
336
+ } catch (err) {
337
+ sendJson(req, res, 400, {
338
+ ok: false,
339
+ error: err instanceof Error ? err.message : String(err),
340
+ code: 'fs_request_failed',
341
+ });
342
+ }
343
+ return;
344
+ }
345
+
153
346
  res.writeHead(404);
154
347
  res.end('not found');
155
348
  });
@@ -158,9 +351,47 @@ export async function startLocalPtyServer(opts = {}) {
158
351
 
159
352
  wss.on('connection', (ws, req) => {
160
353
  const url = new URL(req.url || '/', `http://${host}`);
161
- const sessionCwd = url.searchParams.get('cwd')?.trim() || cwd;
354
+ const cap = extractCapability(req, capability?.capability);
355
+ const qCap = url.searchParams.get('capability') || '';
356
+ const okCap = (capability && qCap && qCap === capability.capability) || cap.ok;
357
+ if (!okCap) {
358
+ ws.send(JSON.stringify({
359
+ type: 'error',
360
+ code: 'WORKSPACE_CAPABILITY_REQUIRED',
361
+ message: 'PTY attach requires workspace capability from /v1/workspace/bootstrap',
362
+ }));
363
+ ws.close();
364
+ return;
365
+ }
366
+
367
+ // Session cwd must stay inside the authorized filesystem root — never escape.
368
+ let sessionCwd = cwd;
369
+ const requestedCwd = url.searchParams.get('cwd')?.trim();
370
+ if (requestedCwd) {
371
+ const abs = path.isAbsolute(requestedCwd) ? path.resolve(requestedCwd) : path.resolve(cwd, requestedCwd);
372
+ if (abs !== cwd) {
373
+ const rel = path.relative(cwd, abs);
374
+ const contained = resolveContainedPath(cwd, rel || '.');
375
+ if (!contained.ok) {
376
+ ws.send(JSON.stringify({
377
+ type: 'error',
378
+ code: 'WORKSPACE_ROOT_MISMATCH',
379
+ message: 'PTY cwd must equal the authorized workspace root (or a contained path).',
380
+ authorized_root: cwd,
381
+ requested: abs,
382
+ }));
383
+ ws.close();
384
+ return;
385
+ }
386
+ sessionCwd = contained.abs;
387
+ } else {
388
+ sessionCwd = cwd;
389
+ }
390
+ }
391
+
162
392
  const cols = parsePort(url.searchParams.get('cols'), 80);
163
393
  const rows = parsePort(url.searchParams.get('rows'), 24);
394
+ const sessionId = `pty_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
164
395
 
165
396
  attachLocalPtySession({
166
397
  ws,
@@ -170,7 +401,17 @@ export async function startLocalPtyServer(opts = {}) {
170
401
  cols,
171
402
  rows,
172
403
  env: process.env,
404
+ sessionId,
173
405
  });
406
+
407
+ ws.send(JSON.stringify({
408
+ type: 'workspace_identity',
409
+ workspace_id: capability.workspace_id,
410
+ root: cwd,
411
+ session_cwd: sessionCwd,
412
+ session_id: sessionId,
413
+ runtimeBaseUrl: capability.runtimeBaseUrl,
414
+ }));
174
415
  });
175
416
 
176
417
  await new Promise((resolve) => {
@@ -178,13 +419,22 @@ export async function startLocalPtyServer(opts = {}) {
178
419
  });
179
420
  const boundPort = Number(httpServer.address()?.port || requestedPort);
180
421
 
422
+ capability = mintWorkspaceCapability({ root: cwd, port: boundPort, host });
423
+ const runtimeFile = writeLocalRuntimeRecord(capability);
424
+
181
425
  return {
182
426
  port: boundPort,
183
427
  host,
184
428
  cwd,
185
429
  shell,
430
+ filesystem,
431
+ capability,
432
+ runtimeFile,
433
+ workspace_id: capability.workspace_id,
186
434
  url: `ws://${host}:${boundPort}`,
187
435
  healthUrl: `http://${host}:${boundPort}/health`,
436
+ fsBaseUrl: `http://${host}:${boundPort}/v1/fs`,
437
+ bootstrapUrl: `http://${host}:${boundPort}/v1/workspace/bootstrap`,
188
438
  close: () =>
189
439
  new Promise((resolve, reject) => {
190
440
  wss.close(() => {
@@ -193,3 +443,29 @@ export async function startLocalPtyServer(opts = {}) {
193
443
  }),
194
444
  };
195
445
  }
446
+
447
+ /**
448
+ * @param {import('http').IncomingMessage} req
449
+ * @param {import('http').ServerResponse} res
450
+ * @param {ReturnType<typeof mintWorkspaceCapability>|null} capability
451
+ */
452
+ function requireCapability(req, res, capability) {
453
+ if (!capability) {
454
+ sendJson(req, res, 503, { ok: false, error: 'capability_not_ready', code: 'WORKSPACE_CAPABILITY_REQUIRED' });
455
+ return false;
456
+ }
457
+ const gate = extractCapability(req, capability.capability);
458
+ if (!gate.ok) {
459
+ sendJson(req, res, 401, {
460
+ ok: false,
461
+ error: 'workspace_capability_required',
462
+ code: 'WORKSPACE_CAPABILITY_REQUIRED',
463
+ authorization: {
464
+ required: ['workspace.capability'],
465
+ bootstrap: '/v1/workspace/bootstrap',
466
+ },
467
+ });
468
+ return false;
469
+ }
470
+ return true;
471
+ }
package/src/mcp/client.js CHANGED
@@ -8,7 +8,7 @@ function clean(value) {
8
8
  export function buildHeaders(serverConfig = {}) {
9
9
  const headers = {
10
10
  'Accept': 'application/json, text/event-stream',
11
- 'User-Agent': 'AgentSam-SDK/2.6.2 (MCP Client)',
11
+ 'User-Agent': 'AgentSam-SDK/2.6.3 (MCP Client)',
12
12
  };
13
13
  const token = clean(serverConfig.auth?.token);
14
14
  if (token) {
@@ -103,7 +103,7 @@ export async function listMcpTools(serverConfig = {}, options = {}) {
103
103
  const transport = new SSEClientTransport(new URL(url), {
104
104
  requestInit: { headers },
105
105
  });
106
- const client = new Client({ name: 'agentsam-sdk', version: '2.6.2' }, { capabilities: {} });
106
+ const client = new Client({ name: 'agentsam-sdk', version: '2.6.3' }, { capabilities: {} });
107
107
  await client.connect(transport);
108
108
  const toolsResult = await client.listTools();
109
109
  await transport.close();
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Onboarding guidance when a user has no usable AI path yet.
3
+ *
4
+ * Paths (any one unlocks AgentSam model options):
5
+ * 1. Local Ollama + at least one model
6
+ * 2. Provider API key (OpenAI / Anthropic / Gemini / …) → their allotted models
7
+ * 3. Signed-in Local Studio identity + Cloudflare OAuth (Workers AI / account tools)
8
+ */
9
+
10
+ export const LOCAL_STUDIO_ORIGIN = 'https://agentsam.inneranimalmedia.com';
11
+
12
+ /** Identity login (Sign in with Cloudflare) — PKCE public client. */
13
+ export const CF_OAUTH_LOGIN_START = `${LOCAL_STUDIO_ORIGIN}/api/oauth/cloudflare/start?next=/agentsam`;
14
+
15
+ /** Resource connector (MCP / Workers / D1) — same CF OAuth client, different callback. */
16
+ export const CF_OAUTH_CONNECT_START = `${LOCAL_STUDIO_ORIGIN}/api/connections/cloudflare/start`;
17
+
18
+ /** Login portal with explicit next (desktop / CLI). */
19
+ export const LOCAL_STUDIO_LOGIN = `${LOCAL_STUDIO_ORIGIN}/auth/login?next=/agentsam`;
20
+
21
+ /**
22
+ * Redirect URIs that must be registered on the Cloudflare OAuth client
23
+ * "AgentSam Local Studio" (PKCE, Token Authentication Method = None).
24
+ */
25
+ export const CF_OAUTH_REQUIRED_REDIRECTS = Object.freeze([
26
+ `${LOCAL_STUDIO_ORIGIN}/api/oauth/cloudflare/callback`,
27
+ `${LOCAL_STUDIO_ORIGIN}/api/connections/cloudflare/callback`,
28
+ 'http://localhost:3000/api/connections/cloudflare/callback',
29
+ 'http://localhost:3000/api/oauth/cloudflare/callback',
30
+ ]);
31
+
32
+ /**
33
+ * @param {{ inventory?: object, ollama?: object, providers?: object[] }} discovered
34
+ * Shape compatible with discoverIngestModelOptions() / collectModelsStatus().
35
+ */
36
+ export function assessAiAccess(discovered = {}) {
37
+ const providers = discovered.inventory?.providers
38
+ || discovered.providers
39
+ || [];
40
+ const configuredProviders = providers.filter((p) => p?.configured === true);
41
+ const ollamaOnline = Boolean(discovered.ollama?.online || discovered.inventory?.local?.online);
42
+ const ollamaModels = discovered.ollama?.models || discovered.inventory?.local?.models || [];
43
+ const hasOllamaModels = ollamaOnline && Array.isArray(ollamaModels) && ollamaModels.length > 0;
44
+ const hasProviderKeys = configuredProviders.length > 0;
45
+ const embedOptions = Array.isArray(discovered.options)
46
+ ? discovered.options.filter((o) => o.value && o.value !== 'none')
47
+ : [];
48
+
49
+ const ready = hasOllamaModels || hasProviderKeys || embedOptions.length > 0;
50
+
51
+ return {
52
+ ready,
53
+ hasOllamaModels,
54
+ hasProviderKeys,
55
+ configuredProviders: configuredProviders.map((p) => p.id),
56
+ ollamaOnline,
57
+ ollamaModelCount: hasOllamaModels ? ollamaModels.length : 0,
58
+ embedOptionCount: embedOptions.length,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Human-readable onboarding block for CLI notes / stderr.
64
+ * @param {ReturnType<typeof assessAiAccess>} access
65
+ * @param {{ wantCloudAi?: boolean }} [opts]
66
+ */
67
+ export function formatAiAccessOnboarding(access, opts = {}) {
68
+ const lines = [];
69
+ if (access.ready) {
70
+ lines.push('AI access: ready');
71
+ if (access.hasProviderKeys) {
72
+ lines.push(` providers ${access.configuredProviders.join(', ')}`);
73
+ }
74
+ if (access.hasOllamaModels) {
75
+ lines.push(` ollama online · ${access.ollamaModelCount} model(s)`);
76
+ }
77
+ if (access.embedOptionCount) {
78
+ lines.push(` embed menu ${access.embedOptionCount} option(s)`);
79
+ }
80
+ return lines.join('\n');
81
+ }
82
+
83
+ lines.push('AI access: not configured yet');
84
+ lines.push('');
85
+ lines.push('Pick ONE path to unlock AgentSam model options:');
86
+ lines.push('');
87
+ lines.push(' 1) Local models (Ollama) — free, offline');
88
+ lines.push(' https://ollama.com/download');
89
+ lines.push(' ollama pull mxbai-embed-large');
90
+ lines.push(' ollama pull qwen2.5-coder');
91
+ lines.push(' agentsam ollama status');
92
+ lines.push('');
93
+ lines.push(' 2) Provider API key — use models your key is allotted');
94
+ lines.push(' agentsam providers');
95
+ lines.push(' (OpenAI · Anthropic · Gemini · Cursor · xAI · Cloudflare token)');
96
+ lines.push('');
97
+ lines.push(' 3) Local Studio identity + Cloudflare OAuth');
98
+ lines.push(' (Workers AI / account tools / MCP connector)');
99
+ lines.push(` Login: ${LOCAL_STUDIO_LOGIN}`);
100
+ lines.push(` CF sign-in: ${CF_OAUTH_LOGIN_START}`);
101
+ lines.push(` CF connect: ${CF_OAUTH_CONNECT_START}`);
102
+ if (opts.wantCloudAi) {
103
+ lines.push('');
104
+ lines.push(' Cloudflare OAuth client must allow these redirect URIs:');
105
+ for (const uri of CF_OAUTH_REQUIRED_REDIRECTS) {
106
+ lines.push(` • ${uri}`);
107
+ }
108
+ }
109
+ lines.push('');
110
+ lines.push('AST/text-only indexing still works with embedding: none ($0).');
111
+ return lines.join('\n');
112
+ }
@@ -315,11 +315,19 @@ export async function discoverCloudflareModels(apiToken, accountId, fetchImpl =
315
315
  .map((row) => {
316
316
  const id = clean(row?.name);
317
317
  const task = cloudflareTaskName(row?.task);
318
- if (!id || task.toLowerCase() !== 'text generation') return null;
318
+ const taskLower = task.toLowerCase();
319
+ // Text Generation = chat/agent pool. Text Embeddings = Vectorize / Workers AI embed pool.
320
+ // Never drop embeddings here — chat allowlisting happens in inventory curation only.
321
+ if (!id || (taskLower !== 'text generation' && taskLower !== 'text embeddings')) return null;
322
+ const isEmbed = taskLower === 'text embeddings';
319
323
  return baseRecord('cloudflare', id, {
320
324
  label: id,
321
325
  source_url: url,
322
- capabilities: { workers_ai: true },
326
+ capabilities: {
327
+ workers_ai: true,
328
+ embeddings: isEmbed,
329
+ agent_runtime: !isEmbed,
330
+ },
323
331
  metadata: {
324
332
  task,
325
333
  author: clean(row?.author) || null,
@@ -42,7 +42,15 @@ export function providerIdForService(serviceName) {
42
42
 
43
43
  export function filterWorkersAiCurated(models = [], options = {}) {
44
44
  if (options.curated === false) return [...models];
45
- return models.filter((row) => CURATED.has(String(row?.provider_model_id || row?.model_id || '')));
45
+ // Chat/agent pool: curated allowlist. Embedding models always pass —
46
+ // Vectorize / Workers AI embed lanes must not be limited by the chat allowlist.
47
+ return models.filter((row) => {
48
+ const id = String(row?.provider_model_id || row?.model_id || '');
49
+ const caps = row?.capabilities || {};
50
+ const task = String(row?.metadata?.task || '').toLowerCase();
51
+ if (caps.embeddings === true || task === 'text embeddings' || /embed/i.test(id)) return true;
52
+ return CURATED.has(id);
53
+ });
46
54
  }
47
55
 
48
56
  /**