@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
@@ -166,6 +166,59 @@ export function createErrorEnvelope(input = {}) {
166
166
  if (!Number.isInteger(envelope.http_status) || envelope.http_status < 100 || envelope.http_status > 599) envelope.http_status = null;
167
167
  if (!Number.isInteger(envelope.grpc_status) || envelope.grpc_status < 0 || envelope.grpc_status > 16) envelope.grpc_status = null;
168
168
  envelope.fingerprint = optional(input.fingerprint, 128) || fingerprintError(envelope);
169
+
170
+ // Optional recovery dimensions (v1-compatible extras — orthogonal to reason enum)
171
+ const failureClass = input.failure_class || null;
172
+ if (failureClass) envelope.failure_class = clean(failureClass);
173
+ if (input.operation && typeof input.operation === 'object') {
174
+ envelope.operation = Object.freeze({
175
+ kind: optional(input.operation.kind, 256),
176
+ action: optional(input.operation.action, 256),
177
+ resource_type: optional(input.operation.resource_type, 128),
178
+ resource_id: optional(input.operation.resource_id, 512),
179
+ read_only: input.operation.read_only === true,
180
+ idempotent: input.operation.idempotent === true,
181
+ side_effect_state: optional(input.operation.side_effect_state, 64) || 'unknown',
182
+ });
183
+ }
184
+ if (input.retry && typeof input.retry === 'object') {
185
+ envelope.retry = Object.freeze({
186
+ allowed: input.retry.allowed == null ? retryable : Boolean(input.retry.allowed),
187
+ strategy: optional(input.retry.strategy, 64) || (retryable ? 'exponential_backoff' : 'none'),
188
+ retry_after_ms: input.retry.retry_after_ms == null ? envelope.retry_after_ms : Math.max(0, Math.round(Number(input.retry.retry_after_ms) || 0)),
189
+ max_attempts: Number.isInteger(input.retry.max_attempts) ? input.retry.max_attempts : null,
190
+ requires_idempotency_key: input.retry.requires_idempotency_key === true,
191
+ });
192
+ }
193
+ if (input.fallback && typeof input.fallback === 'object') {
194
+ envelope.fallback = Object.freeze({
195
+ allowed: Boolean(input.fallback.allowed),
196
+ strategy: optional(input.fallback.strategy, 64),
197
+ preserve_semantics: input.fallback.preserve_semantics !== false,
198
+ target: optional(input.fallback.target, 256),
199
+ });
200
+ }
201
+ if (input.notify && typeof input.notify === 'object') {
202
+ envelope.notify = Object.freeze({
203
+ user: optional(input.notify.user, 64) || 'immediately',
204
+ operator: optional(input.notify.operator, 64) || 'never',
205
+ agent: optional(input.notify.agent, 64) || 'stop',
206
+ });
207
+ }
208
+ if (input.cause && typeof input.cause === 'object') {
209
+ envelope.cause = input.cause;
210
+ }
211
+ if (input.status === 'partial' || input.status === 'success' || input.status === 'failure') {
212
+ envelope.status = input.status;
213
+ }
214
+ if (input.completed != null || input.failed != null) {
215
+ envelope.partial = Object.freeze({
216
+ completed: Number(input.completed) || 0,
217
+ failed: Number(input.failed) || 0,
218
+ failures: Array.isArray(input.failures) ? input.failures : [],
219
+ });
220
+ }
221
+
169
222
  return deepFreeze(envelope);
170
223
  }
171
224
 
@@ -5,6 +5,7 @@ export * from './normalize.js';
5
5
  export * from './redaction.js';
6
6
  export * from './fingerprint.js';
7
7
  export * from './retry.js';
8
+ export * from './recovery.js';
8
9
  export * from './transport.js';
9
10
  export * from './render.js';
10
11
  export * from './adapters/index.js';
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Recovery policy — separates "what failed" from "what to do".
3
+ *
4
+ * ErrorEnvelope → planRecovery → RecoveryPlan → SAM executes
5
+ *
6
+ * Critical rule: unknown side_effect_state ⇒ reconcile before retry.
7
+ */
8
+
9
+ export const FAILURE_CLASS = Object.freeze({
10
+ INPUT: 'input',
11
+ AUTHENTICATION: 'authentication',
12
+ AUTHORIZATION: 'authorization',
13
+ POLICY: 'policy',
14
+ DISCOVERY: 'discovery',
15
+ RESOLUTION: 'resolution',
16
+ TRANSPORT: 'transport',
17
+ PROTOCOL: 'protocol',
18
+ TIMEOUT: 'timeout',
19
+ RATE_LIMIT: 'rate_limit',
20
+ CAPACITY: 'capacity',
21
+ RESOURCE: 'resource',
22
+ FILESYSTEM: 'filesystem',
23
+ PERSISTENCE: 'persistence',
24
+ CONSISTENCY: 'consistency',
25
+ CONFLICT: 'conflict',
26
+ DEPENDENCY: 'dependency',
27
+ PROCESS: 'process',
28
+ IPC: 'ipc',
29
+ DATA_INTEGRITY: 'data_integrity',
30
+ CANCELLED: 'cancelled',
31
+ UNKNOWN: 'unknown',
32
+ });
33
+
34
+ export const SIDE_EFFECT_STATE = Object.freeze({
35
+ NONE: 'none',
36
+ NOT_STARTED: 'not_started',
37
+ CONFIRMED_NOT_APPLIED: 'confirmed_not_applied',
38
+ CONFIRMED_APPLIED: 'confirmed_applied',
39
+ PARTIALLY_APPLIED: 'partially_applied',
40
+ UNKNOWN: 'unknown',
41
+ });
42
+
43
+ export const RECOVERY_SCHEMA = 'agentsam.recovery.v1';
44
+
45
+ const NO_RETRY_OWNERS = new Set(['policy']);
46
+ const CANCEL_REASONS = new Set([
47
+ 'cancelled_by_user',
48
+ 'cancelled_by_agent',
49
+ 'cancelled_by_policy',
50
+ 'cancelled_by_timeout',
51
+ 'cancelled_by_parent',
52
+ 'cancelled',
53
+ ]);
54
+
55
+ /**
56
+ * Infer failure_class from reason/code when not supplied.
57
+ * @param {object} error
58
+ */
59
+ export function inferFailureClass(error = {}) {
60
+ const given = error.failure_class;
61
+ if (given && Object.values(FAILURE_CLASS).includes(given)) return given;
62
+ const reason = String(error.reason || '');
63
+ const code = String(error.code || '');
64
+ if (CANCEL_REASONS.has(reason) || code === 'CANCELLED') return FAILURE_CLASS.CANCELLED;
65
+ if (/^auth_|unauthenticated|credential/.test(reason) || code === 'UNAUTHENTICATED') return FAILURE_CLASS.AUTHENTICATION;
66
+ if (/permission|authorization|capability/.test(reason) || code === 'PERMISSION_DENIED') return FAILURE_CLASS.AUTHORIZATION;
67
+ if (/policy/.test(reason)) return FAILURE_CLASS.POLICY;
68
+ if (/rate_limit|resource_exhausted|quota|budget/.test(reason) || code === 'RESOURCE_EXHAUSTED') return FAILURE_CLASS.RATE_LIMIT;
69
+ if (/timeout|deadline/.test(reason) || code === 'DEADLINE_EXCEEDED') return FAILURE_CLASS.TIMEOUT;
70
+ if (/^fs_|filesystem|path_escape|version_conflict/.test(reason)) return FAILURE_CLASS.FILESYSTEM;
71
+ if (/^db_|persistence|sqlite|d1/.test(reason)) return FAILURE_CLASS.PERSISTENCE;
72
+ if (/conflict|etag|precondition|serialization/.test(reason) || code === 'ABORTED') return FAILURE_CLASS.CONFLICT;
73
+ if (/^pty_|process_|ipc_/.test(reason)) return FAILURE_CLASS.PROCESS;
74
+ if (/transport|connection|dns|tls|websocket|grpc_unavailable/.test(reason) || code === 'UNAVAILABLE') {
75
+ return FAILURE_CLASS.TRANSPORT;
76
+ }
77
+ if (/corrupt|checksum|integrity|data_loss/.test(reason) || code === 'DATA_LOSS') return FAILURE_CLASS.DATA_INTEGRITY;
78
+ if (/input_|invalid_argument|malformed/.test(reason) || code === 'INVALID_ARGUMENT') return FAILURE_CLASS.INPUT;
79
+ return FAILURE_CLASS.UNKNOWN;
80
+ }
81
+
82
+ /**
83
+ * Infer side_effect_state for the failed operation.
84
+ * @param {object} error
85
+ * @param {object} [operationContext]
86
+ */
87
+ export function inferSideEffectState(error = {}, operationContext = {}) {
88
+ const op = error.operation || operationContext.operation || {};
89
+ if (op.side_effect_state) return op.side_effect_state;
90
+ if (op.read_only === true || op.side_effect_state === SIDE_EFFECT_STATE.NONE) {
91
+ return SIDE_EFFECT_STATE.NONE;
92
+ }
93
+ if (op.idempotent === true && error.retryable) {
94
+ return SIDE_EFFECT_STATE.CONFIRMED_NOT_APPLIED;
95
+ }
96
+ // Timeouts / connection drops on mutating ops → outcome unknown
97
+ const reason = String(error.reason || '');
98
+ const code = String(error.code || '');
99
+ if (
100
+ code === 'DEADLINE_EXCEEDED'
101
+ || /timeout|connection_reset|connection_closed|truncated/.test(reason)
102
+ ) {
103
+ if (op.read_only) return SIDE_EFFECT_STATE.NONE;
104
+ return SIDE_EFFECT_STATE.UNKNOWN;
105
+ }
106
+ if (error.retryable === false && /permission|auth|input|policy/.test(reason)) {
107
+ return SIDE_EFFECT_STATE.CONFIRMED_NOT_APPLIED;
108
+ }
109
+ return op.kind ? SIDE_EFFECT_STATE.UNKNOWN : SIDE_EFFECT_STATE.NONE;
110
+ }
111
+
112
+ /**
113
+ * Central recovery planner — one policy for CLI / Worker / Studio / MCP.
114
+ *
115
+ * @param {object} error ErrorEnvelope (v1+)
116
+ * @param {object} [operationContext]
117
+ * @returns {object} RecoveryPlan
118
+ */
119
+ export function planRecovery(error = {}, operationContext = {}) {
120
+ const failure_class = inferFailureClass(error);
121
+ const side_effect_state = inferSideEffectState(error, operationContext);
122
+ const attempt = Number(operationContext.attempt || error.retry?.attempt || 1);
123
+ const maxAttempts = Number(
124
+ operationContext.max_attempts
125
+ ?? error.retry?.max_attempts
126
+ ?? (error.retryable ? 4 : 1),
127
+ );
128
+ const evidence = [];
129
+ evidence.push(`failure_class=${failure_class}`);
130
+ evidence.push(`side_effect_state=${side_effect_state}`);
131
+ evidence.push(`reason=${error.reason || 'unknown'}`);
132
+
133
+ /** @type {object} */
134
+ let plan = {
135
+ schema: RECOVERY_SCHEMA,
136
+ error_fingerprint: error.fingerprint || null,
137
+ disposition: 'abort',
138
+ attempts: { current: attempt, maximum: maxAttempts },
139
+ delay_ms: null,
140
+ fallback: null,
141
+ notify: {
142
+ user: 'immediately',
143
+ operator: 'never',
144
+ agent: 'stop',
145
+ },
146
+ reason: error.reason || 'unknown',
147
+ evidence,
148
+ };
149
+
150
+ // Cancellations are not defects
151
+ if (failure_class === FAILURE_CLASS.CANCELLED) {
152
+ return freezePlan({
153
+ ...plan,
154
+ disposition: 'abort',
155
+ notify: { user: 'never', operator: 'never', agent: 'stop' },
156
+ reason: 'cancelled',
157
+ evidence: [...evidence, 'cancellation_is_not_a_defect'],
158
+ });
159
+ }
160
+
161
+ // Ambiguous side effects: NEVER blind retry
162
+ if (
163
+ side_effect_state === SIDE_EFFECT_STATE.UNKNOWN
164
+ || side_effect_state === SIDE_EFFECT_STATE.PARTIALLY_APPLIED
165
+ ) {
166
+ return freezePlan({
167
+ ...plan,
168
+ disposition: 'reconcile',
169
+ notify: { user: 'immediately', operator: 'warning', agent: 'wait' },
170
+ reason: 'side_effect_uncertain',
171
+ evidence: [...evidence, 'reconcile_before_retry'],
172
+ });
173
+ }
174
+
175
+ // Policy / auth hard stops
176
+ if (
177
+ failure_class === FAILURE_CLASS.POLICY
178
+ || failure_class === FAILURE_CLASS.AUTHORIZATION
179
+ || NO_RETRY_OWNERS.has(error.resolution_owner)
180
+ ) {
181
+ return freezePlan({
182
+ ...plan,
183
+ disposition: 'await_user',
184
+ notify: { user: 'immediately', operator: 'never', agent: 'wait' },
185
+ reason: 'policy_or_auth_blocks_retry',
186
+ });
187
+ }
188
+
189
+ if (failure_class === FAILURE_CLASS.AUTHENTICATION) {
190
+ return freezePlan({
191
+ ...plan,
192
+ disposition: 'await_user',
193
+ notify: { user: 'immediately', operator: 'never', agent: 'wait' },
194
+ reason: 'authentication_required',
195
+ });
196
+ }
197
+
198
+ if (failure_class === FAILURE_CLASS.INPUT || failure_class === FAILURE_CLASS.CONFLICT) {
199
+ const disposition = failure_class === FAILURE_CLASS.CONFLICT && error.retryable
200
+ ? 'retry'
201
+ : 'await_user';
202
+ return freezePlan({
203
+ ...plan,
204
+ disposition,
205
+ delay_ms: disposition === 'retry' ? 0 : null,
206
+ attempts: { current: attempt, maximum: Math.min(maxAttempts, 2) },
207
+ notify: {
208
+ user: disposition === 'await_user' ? 'immediately' : 'after_retry_exhausted',
209
+ operator: 'never',
210
+ agent: disposition === 'retry' ? 'retry' : 'wait',
211
+ },
212
+ reason: failure_class === FAILURE_CLASS.CONFLICT ? 'optimistic_concurrency' : 'invalid_input',
213
+ });
214
+ }
215
+
216
+ // Safe retry path
217
+ if (error.retryable && attempt < maxAttempts) {
218
+ const delay = retryDelayFor(error, attempt, failure_class);
219
+ return freezePlan({
220
+ ...plan,
221
+ disposition: failure_class === FAILURE_CLASS.TRANSPORT || /pty_|websocket|connection/.test(String(error.reason))
222
+ ? 'reconnect'
223
+ : 'retry',
224
+ delay_ms: delay,
225
+ notify: {
226
+ user: attempt >= 2 ? 'on_degradation' : 'never',
227
+ operator: 'never',
228
+ agent: 'retry',
229
+ },
230
+ reason: 'retry_budget_remaining',
231
+ evidence: [...evidence, `delay_ms=${delay}`],
232
+ });
233
+ }
234
+
235
+ // Fallback if permitted and semantics-preserving
236
+ const fallbackAllowed = error.fallback?.allowed === true
237
+ || operationContext.fallback_allowed === true;
238
+ const preserve = error.fallback?.preserve_semantics !== false
239
+ && operationContext.preserve_semantics !== false;
240
+ if (fallbackAllowed && preserve) {
241
+ return freezePlan({
242
+ ...plan,
243
+ disposition: 'fallback',
244
+ fallback: {
245
+ kind: error.fallback?.strategy || operationContext.fallback_strategy || 'degraded_mode',
246
+ target: error.fallback?.target || operationContext.fallback_target || null,
247
+ },
248
+ notify: { user: 'on_degradation', operator: 'warning', agent: 'continue' },
249
+ reason: 'semantic_fallback_allowed',
250
+ });
251
+ }
252
+
253
+ // Replan if GOAP/A* context available
254
+ if (operationContext.replan_allowed) {
255
+ return freezePlan({
256
+ ...plan,
257
+ disposition: 'replan',
258
+ notify: { user: 'on_degradation', operator: 'never', agent: 'replan' },
259
+ reason: 'retry_exhausted_replan',
260
+ });
261
+ }
262
+
263
+ // Terminal block
264
+ return freezePlan({
265
+ ...plan,
266
+ disposition: error.resolution_owner === 'user' ? 'await_user' : 'abort',
267
+ notify: {
268
+ user: 'immediately',
269
+ operator: error.severity === 'blocking_internal' ? 'incident' : 'warning',
270
+ agent: 'stop',
271
+ },
272
+ reason: 'recovery_exhausted',
273
+ });
274
+ }
275
+
276
+ function retryDelayFor(error, attempt, failureClass) {
277
+ if (Number.isFinite(error.retry_after_ms) && error.retry_after_ms >= 0) {
278
+ return Math.round(error.retry_after_ms);
279
+ }
280
+ if (failureClass === FAILURE_CLASS.RATE_LIMIT) {
281
+ return Math.min(60_000, Math.round(1000 * (2 ** Math.min(6, attempt - 1))));
282
+ }
283
+ if (failureClass === FAILURE_CLASS.TRANSPORT || failureClass === FAILURE_CLASS.TIMEOUT) {
284
+ const base = 250;
285
+ const capped = Math.min(8_000, base * (2 ** Math.min(5, attempt - 1)));
286
+ const jitter = Math.floor(Math.random() * Math.max(50, capped * 0.2));
287
+ return capped + jitter;
288
+ }
289
+ if (failureClass === FAILURE_CLASS.CONFLICT) return 0;
290
+ return Math.min(4_000, 500 * (2 ** Math.min(4, attempt - 1)));
291
+ }
292
+
293
+ function freezePlan(plan) {
294
+ return Object.freeze({
295
+ ...plan,
296
+ attempts: Object.freeze({ ...plan.attempts }),
297
+ notify: Object.freeze({ ...plan.notify }),
298
+ fallback: plan.fallback ? Object.freeze({ ...plan.fallback }) : null,
299
+ evidence: Object.freeze([...(plan.evidence || [])]),
300
+ });
301
+ }
@@ -6,13 +6,13 @@ const finiteVector = (vector, dimensions) => {
6
6
  return vector;
7
7
  };
8
8
 
9
- function httpAdapter({ id, env, models, request, credentials = env }) {
9
+ function httpAdapter({ id, env, models, request, credentials = env, allowAnyModel = false }) {
10
10
  return Object.freeze({
11
11
  id,
12
12
  capabilities: () => ({ id, operational: Boolean(credentials()), models, credentials: credentials() ? 'available' : 'missing' }),
13
13
  validate(profile) {
14
14
  if (clean(profile?.provider) !== id) throw new Error(`provider_profile_mismatch:${id}`);
15
- if (!models.includes(clean(profile?.model))) throw new Error(`provider_model_unsupported:${id}:${profile?.model}`);
15
+ if (!allowAnyModel && !models.includes(clean(profile?.model))) throw new Error(`provider_model_unsupported:${id}:${profile?.model}`);
16
16
  if (!Number.isInteger(profile?.dimensions) || profile.dimensions < 1) throw new Error('provider_dimensions_required');
17
17
  if (!credentials()) throw new Error(`provider_credentials_unavailable:${id}`);
18
18
  },
@@ -70,13 +70,25 @@ export function createWorkersAiProvider({ binding } = {}) {
70
70
  });
71
71
  }
72
72
 
73
- export function createOllamaProvider({ endpoint = process.env.OLLAMA_HOST || 'http://127.0.0.1:11434', fetchImpl = globalThis.fetch } = {}) {
74
- return httpAdapter({ id: 'ollama', env: 'OLLAMA_HOST', models: ['nomic-embed-text', 'mxbai-embed-large'], credentials: () => endpoint,
73
+ export function createOllamaProvider({ endpoint = process.env.OLLAMA_HOST || 'http://127.0.0.1:11434', fetchImpl = globalThis.fetch, models } = {}) {
74
+ // Model allowlist is advisory only — live `ollama list` / tags are authoritative at selection time.
75
+ const known = Array.isArray(models) && models.length ? models : ['mxbai-embed-large', 'nomic-embed-text'];
76
+ return httpAdapter({
77
+ id: 'ollama',
78
+ env: 'OLLAMA_HOST',
79
+ models: known,
80
+ allowAnyModel: true,
81
+ credentials: () => endpoint,
75
82
  request: async (text, profile) => {
76
- const response = await fetchImpl(new URL('/api/embed', endpoint), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ model: profile.model, input: text }) });
83
+ const response = await fetchImpl(new URL('/api/embed', endpoint), {
84
+ method: 'POST',
85
+ headers: { 'content-type': 'application/json' },
86
+ body: JSON.stringify({ model: profile.model, input: text }),
87
+ });
77
88
  if (!response.ok) throw new Error(`provider_request_failed:ollama:${response.status}`);
78
89
  return finiteVector((await response.json()).embeddings?.[0], profile.dimensions);
79
- } });
90
+ },
91
+ });
80
92
  }
81
93
 
82
94
  export function createProviderRegistry(options = {}) {
@@ -220,6 +220,15 @@ export async function handleCloudflareConnectionRequest(request, env) {
220
220
  state,
221
221
  codeChallenge: challenge,
222
222
  });
223
+ // Browser navigation → 302 to Cloudflare. XHR/fetch (Integrations UI) → JSON.
224
+ const accept = String(request.headers.get('accept') || '');
225
+ const mode = String(request.headers.get('sec-fetch-mode') || '');
226
+ const wantsJson = accept.includes('application/json')
227
+ || mode === 'cors'
228
+ || request.headers.get('x-agentsam-oauth') === 'json';
229
+ if (!wantsJson) {
230
+ return Response.redirect(authorize, 302);
231
+ }
223
232
  return json({ ok: true, authorize_url: authorize, callback: CLOUDFLARE_CALLBACK_PATH });
224
233
  }
225
234
 
@@ -64,7 +64,10 @@ describe('cloudflare connector', () => {
64
64
  };
65
65
  const response = await handleCloudflareConnectionRequest(
66
66
  new Request('https://agentsam.example/api/connections/cloudflare/start?return_to=https://untrusted.example/after', {
67
- headers: { authorization: 'Bearer fixture' },
67
+ headers: {
68
+ authorization: 'Bearer fixture',
69
+ accept: 'application/json',
70
+ },
68
71
  }),
69
72
  {
70
73
  DB,
@@ -82,6 +85,28 @@ describe('cloudflare connector', () => {
82
85
  assert.equal(stateInsert.args.at(-1), null);
83
86
  });
84
87
 
88
+ it('redirects browser starts to Cloudflare authorization', async () => {
89
+ const oauthState = new Map();
90
+ const response = await handleCloudflareConnectionRequest(
91
+ new Request('https://agentsam.example/api/connections/cloudflare/start', {
92
+ headers: { authorization: 'Bearer fixture' },
93
+ }),
94
+ {
95
+ fixtureSessions: new Map([['fixture', 'user_123']]),
96
+ oauthState,
97
+ CLOUDFLARE_OAUTH_CLIENT_ID: 'real-client-id',
98
+ },
99
+ );
100
+
101
+ assert.equal(response.status, 302);
102
+ const location = new URL(response.headers.get('location'));
103
+ assert.equal(location.origin, 'https://dash.cloudflare.com');
104
+ assert.equal(location.pathname, '/oauth2/auth');
105
+ assert.equal(location.searchParams.get('client_id'), 'real-client-id');
106
+ assert.equal(location.searchParams.get('redirect_uri'), 'https://agentsam.example/api/connections/cloudflare/callback');
107
+ assert.equal(oauthState.size, 1);
108
+ });
109
+
85
110
  it('consumes callback state and stores only encrypted OAuth tokens', async () => {
86
111
  const now = Math.floor(Date.now() / 1000);
87
112
  const calls = [];
@@ -6,7 +6,7 @@
6
6
  "status": "extracted",
7
7
  "package": {
8
8
  "name": "@inneranimalmedia/agentsam-sdk-identity",
9
- "version": "2.6.2",
9
+ "version": "2.6.3",
10
10
  "directory": "packages/identity"
11
11
  },
12
12
  "provides": [
@@ -46,11 +46,20 @@
46
46
  "provider": "github"
47
47
  },
48
48
  {
49
- "path": "/api/oauth/iam/start",
49
+ "path": "/api/oauth/inneranimalmedia/start",
50
50
  "kind": "api",
51
51
  "access": "public",
52
52
  "provider": "inneranimalmedia",
53
- "oauth_provider_key": "iam"
53
+ "oauth_provider_key": "iam",
54
+ "aliases": ["/api/oauth/iam/start"]
55
+ },
56
+ {
57
+ "path": "/api/oauth/inneranimalmedia/callback",
58
+ "kind": "api",
59
+ "access": "public",
60
+ "provider": "inneranimalmedia",
61
+ "oauth_provider_key": "iam",
62
+ "aliases": ["/api/oauth/iam/callback"]
54
63
  },
55
64
  {
56
65
  "path": "/api/oauth/cloudflare/start",
@@ -0,0 +1,50 @@
1
+ # Portable Identity Architecture (pre-commit return)
2
+
3
+ ## Invariants
4
+
5
+ 1. Portable SDK owns **semantic route IDs** (`identity.login`), never host paths.
6
+ 2. App manifests own **HTTP projections**.
7
+ 3. Auth-capable apps MUST declare authenticated / login / recovery / OAuth callback.
8
+ 4. OAuth **transactions** persist `app_id` + `return_to` (required).
9
+ 5. Post-login: valid `return_to` → authenticated entry → recovery+error → **throw** (never `/`).
10
+ 6. No `DASHBOARD_AFTER_LOGIN_PATH`, no `LOCAL_STUDIO_APP` in identity, no `APP_HOME_PATH` env.
11
+ 7. Missing shell/route → package verification failure.
12
+
13
+ ## Artifacts delivered
14
+
15
+ | Deliverable | Location |
16
+ |---|---|
17
+ | IdentityStore + errors | `packages/identity/src/contracts/identity-store.js` |
18
+ | Semantic IDs | `packages/identity/src/contracts/route-ids.js` |
19
+ | Route projection | `packages/identity/src/contracts/route-projection.js` |
20
+ | Fail-closed post-auth | `packages/identity/src/server/post-auth.js` |
21
+ | SESSION_POLICY | `packages/identity/src/core/session-policy.js` |
22
+ | SQLite core pack | `packages/identity/migrations/sqlite/001_identity_core.sql` |
23
+ | SQLite oauth-client | `packages/identity/migrations/sqlite/002_identity_oauth_client.sql` |
24
+ | SQLite oauth-server (optional) | `packages/identity/migrations/sqlite/003_identity_oauth_server.sql` |
25
+ | D1 mapping | `packages/identity/migrations/D1_SCHEMA_MAPPING.md` |
26
+ | createSqliteIdentityAdapter | `packages/identity/src/adapters/sqlite/index.js` |
27
+ | App verify | `packages/identity/src/app/verify-app.js` |
28
+ | App auth contract (Local Studio) | `apps/local-studio/agentsam.app.json` |
29
+ | Tests | `packages/identity/tests/portable-identity-architecture.test.mjs` |
30
+
31
+ ## Redirect resolution
32
+
33
+ ```
34
+ transaction.return_to → owned by app?
35
+ yes → use
36
+ no → app.authenticated
37
+ missing → identity.recovery?error=…
38
+ none → IdentityRoutingError('AUTH_DESTINATION_UNRESOLVED')
39
+ ```
40
+
41
+ ## Local-only path
42
+
43
+ ```
44
+ applySqliteIdentityMigrations(db) // core + oauth-client
45
+ createSqliteIdentityAdapter(db)
46
+ createIdentityService({ adapter, app, routeRegistry })
47
+ ```
48
+
49
+ Zero Cloudflare / IAM / D1 required for password + session + OAuth transaction store.
50
+ Provider secrets → encrypted vault via `credential_ref` on `identity_provider_connections`.
@@ -0,0 +1,31 @@
1
+ # Identity schema mapping — portable ↔ Cloudflare D1
2
+
3
+ Portable SQLite (`packages/identity/migrations/sqlite/`) is the product schema.
4
+ The D1 adapter maps existing hosted table names without requiring a big-bang rename.
5
+
6
+ | Portable (SQLite) | Hosted D1 (current) | Notes |
7
+ |---|---|---|
8
+ | `identity_users` | `auth_users` | Same columns |
9
+ | `identity_external_accounts` | `account_identities` | `user_id` ↔ `account_id` |
10
+ | `identity_sessions` | `auth_sessions` | Same columns |
11
+ | `identity_auth_events` | `auth_event_log` | Same columns |
12
+ | `identity_oauth_transactions` | `identity_oauth_states` → migrate to `identity_oauth_transactions` | Requires `app_id NOT NULL`; no silent pre-app_id fallback |
13
+ | `identity_provider_connections` | (new / connector tables) | `credential_ref` → vault/KMS; never store raw tokens in D1 rows |
14
+ | `identity_app_registry` | cache of `agentsam.app.json` | Disk manifests remain SSOT |
15
+ | `identity_route_registry` | projection cache | Disk manifests remain SSOT |
16
+ | `identity_oauth_clients` … | optional `identity.oauth-server` pack | Only if product is an AS |
17
+
18
+ ## Schema version
19
+
20
+ Both adapters read `identity_schema_meta.schema_version` (SQLite) or
21
+ `PRAGMA`/meta row. Missing `app_id` on OAuth transactions is an
22
+ `IdentitySchemaError('IDENTITY_SCHEMA_MIGRATION_REQUIRED')`, not a soft
23
+ downgrade.
24
+
25
+ ## Packs
26
+
27
+ | Pack | Apply when |
28
+ |---|---|
29
+ | `identity.core` | Always (local + hosted) |
30
+ | `identity.oauth-client` | Signing into external IdPs |
31
+ | `identity.oauth-server` | Product issues its own codes/tokens |