@sema-agent/server 7.139.0 → 7.140.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/MIGRATION.md +181 -0
  2. package/USAGE.md +19 -13
  3. package/dist/adoption/runner.js +1 -1
  4. package/dist/adoption/sql.js +1 -1
  5. package/dist/approval-card.d.ts +6 -6
  6. package/dist/bench/s1/live-deps.js +3 -2
  7. package/dist/bench/s1/row.d.ts +5 -2
  8. package/dist/bench/s1/row.js +2 -22
  9. package/dist/boot/adoption.js +1 -1
  10. package/dist/boot/execution-env.js +12 -10
  11. package/dist/boot/execution-lane.d.ts +8 -0
  12. package/dist/boot/execution-lane.js +6 -1
  13. package/dist/boot/memory-boundary.d.ts +14 -51
  14. package/dist/boot/memory-boundary.js +10 -20
  15. package/dist/boot/memory-consolidation.d.ts +15 -11
  16. package/dist/boot/memory-consolidation.js +23 -12
  17. package/dist/boot/resolve-spec.d.ts +3 -5
  18. package/dist/boot/resolve-spec.js +2 -4
  19. package/dist/boot/runner-deps.d.ts +3 -3
  20. package/dist/boot/runner-deps.js +1 -0
  21. package/dist/boot/stage-03-store-backend.d.ts +2 -6
  22. package/dist/boot/stage-03-store-backend.js +2 -3
  23. package/dist/boot/stage-04-stores.d.ts +2 -6
  24. package/dist/boot/stage-04-stores.js +5 -5
  25. package/dist/boot/stage-05-execution-env.d.ts +2 -6
  26. package/dist/boot/stage-06-runners.d.ts +2 -6
  27. package/dist/boot/stage-06-runners.js +2 -2
  28. package/dist/boot/stage-07-capability-layer.d.ts +2 -6
  29. package/dist/boot/stage-08-reapers.d.ts +2 -6
  30. package/dist/boot/stage-09-leader.d.ts +2 -6
  31. package/dist/boot/stage-10-http-server.d.ts +2 -6
  32. package/dist/boot/stage-10-http-server.js +5 -4
  33. package/dist/boot/stores.d.ts +3 -6
  34. package/dist/boot/stores.js +27 -40
  35. package/dist/boot/task-list-lane.d.ts +1 -1
  36. package/dist/boot/task-list-lane.js +1 -1
  37. package/dist/boot/web-admin.js +35 -9
  38. package/dist/capabilities/execution-lane.d.ts +2 -2
  39. package/dist/config-catalog.js +2 -1
  40. package/dist/config-provider.d.ts +14 -13
  41. package/dist/config-provider.js +24 -18
  42. package/dist/config-types.d.ts +11 -2
  43. package/dist/config.js +2 -1
  44. package/dist/delegated-session-owner.d.ts +2 -1
  45. package/dist/delegated-session-owner.js +1 -1
  46. package/dist/deployment-shape.d.ts +9 -8
  47. package/dist/deployment-shape.js +4 -4
  48. package/dist/execution-lane-caps.d.ts +97 -106
  49. package/dist/execution-lane-caps.js +67 -83
  50. package/dist/http/memory-compliance-failure-reply.d.ts +3 -1
  51. package/dist/http/memory-compliance-failure-reply.js +5 -0
  52. package/dist/http/memory-layer-reply.d.ts +30 -0
  53. package/dist/http/memory-layer-reply.js +24 -0
  54. package/dist/http/resume-legs.js +13 -16
  55. package/dist/http/retired-service-deps.d.ts +1 -1
  56. package/dist/http/retired-service-deps.js +1 -1
  57. package/dist/http/routes/adoption.js +1 -1
  58. package/dist/http/routes/memory-bundle.js +3 -0
  59. package/dist/http/routes/memory-compliance.js +8 -1
  60. package/dist/http/routes/memory-consolidation.js +12 -1
  61. package/dist/http/routes/memory-origin.js +14 -2
  62. package/dist/http/routes/memory-policy.js +5 -0
  63. package/dist/http/routes/sessions.js +28 -11
  64. package/dist/http/routes/tasks.js +4 -2
  65. package/dist/http/server.d.ts +5 -25
  66. package/dist/http/server.js +2 -2
  67. package/dist/identity-source.d.ts +1 -1
  68. package/dist/identity-source.js +1 -1
  69. package/dist/lane-home-probe.d.ts +68 -42
  70. package/dist/lane-home-probe.js +53 -30
  71. package/dist/leader/wire.js +5 -4
  72. package/dist/memory-bundle-engine.d.ts +16 -9
  73. package/dist/memory-bundle-engine.js +29 -6
  74. package/dist/memory-operator-faces.d.ts +85 -30
  75. package/dist/memory-operator-faces.js +136 -30
  76. package/dist/memory-project-layer.d.ts +3 -21
  77. package/dist/memory-project-layer.js +11 -8
  78. package/dist/memory-scope-stores.d.ts +73 -0
  79. package/dist/memory-scope-stores.js +86 -0
  80. package/dist/memory-scope.d.ts +29 -5
  81. package/dist/memory-scope.js +16 -4
  82. package/dist/memory-sync-client.d.ts +20 -7
  83. package/dist/memory-sync-client.js +31 -13
  84. package/dist/plugins/adoption-log-sql.js +4 -4
  85. package/dist/plugins/approval-ask-store-sql.js +14 -14
  86. package/dist/plugins/approval-nonce-store.js +4 -4
  87. package/dist/plugins/blob-backend.d.ts +4 -4
  88. package/dist/plugins/blob-backend.js +7 -7
  89. package/dist/plugins/breaker-state-sql.js +2 -2
  90. package/dist/plugins/checkpoint-index-columns.js +2 -2
  91. package/dist/plugins/checkpoint-store-sql.js +16 -16
  92. package/dist/plugins/device-store-sql.js +19 -19
  93. package/dist/plugins/ensure-index.d.ts +1 -2
  94. package/dist/plugins/execution-placement-sql.d.ts +8 -8
  95. package/dist/plugins/execution-placement-sql.js +8 -8
  96. package/dist/plugins/file-history-store-sql.d.ts +2 -2
  97. package/dist/plugins/file-history-store-sql.js +4 -4
  98. package/dist/plugins/image-bake-store-sql.js +9 -9
  99. package/dist/plugins/image-index-sql.js +11 -11
  100. package/dist/plugins/leader-run-store-sql.js +4 -4
  101. package/dist/plugins/mailbox-store-sql.d.ts +2 -1
  102. package/dist/plugins/mailbox-store-sql.js +4 -4
  103. package/dist/plugins/memory-engine-tidb.js +5 -5
  104. package/dist/plugins/memory-optout-grant-store-sql.js +1 -1
  105. package/dist/plugins/memory-sync-store-tidb.js +3 -3
  106. package/dist/plugins/one-time-migrations.d.ts +10 -9
  107. package/dist/plugins/one-time-migrations.js +36 -36
  108. package/dist/plugins/org-delivery-quota-store.js +2 -2
  109. package/dist/plugins/outcome-ledger-sql.js +2 -2
  110. package/dist/plugins/permission-rule-store-sql.d.ts +4 -4
  111. package/dist/plugins/permission-rule-store-sql.js +16 -16
  112. package/dist/plugins/remote-env-adb.d.ts +7 -24
  113. package/dist/plugins/remote-env-adb.js +22 -5
  114. package/dist/plugins/remote-env-device.d.ts +2 -28
  115. package/dist/plugins/remote-env-device.js +2 -1
  116. package/dist/plugins/remote-env-e2b.d.ts +18 -26
  117. package/dist/plugins/remote-env-e2b.js +51 -4
  118. package/dist/plugins/remote-env-file-error.d.ts +1 -1
  119. package/dist/plugins/remote-env-file-error.js +8 -2
  120. package/dist/plugins/remote-env-host.d.ts +19 -35
  121. package/dist/plugins/remote-env-host.js +26 -3
  122. package/dist/plugins/remote-env-k8s.d.ts +3 -14
  123. package/dist/plugins/remote-env-k8s.js +3 -2
  124. package/dist/plugins/remote-env-local-docker.d.ts +4 -15
  125. package/dist/plugins/remote-env-local-docker.js +5 -4
  126. package/dist/plugins/remote-env-ssh.d.ts +4 -24
  127. package/dist/plugins/remote-env-ssh.js +4 -3
  128. package/dist/plugins/resume-anchor-store-sql.js +2 -2
  129. package/dist/plugins/retention-lane-store-sql.d.ts +1 -1
  130. package/dist/plugins/retention-lane-store-sql.js +2 -2
  131. package/dist/plugins/retention-store-sql.d.ts +1 -1
  132. package/dist/plugins/retention-store-sql.js +6 -6
  133. package/dist/plugins/run-store-sql.js +15 -15
  134. package/dist/plugins/schema-consistency.d.ts +6 -6
  135. package/dist/plugins/schema-consistency.js +1 -1
  136. package/dist/plugins/schema-ensure-legs.d.ts +2 -3
  137. package/dist/plugins/schema-ensure-legs.js +2 -2
  138. package/dist/plugins/schema-live-columns.d.ts +1 -1
  139. package/dist/plugins/session-capture-record-store-sql.js +1 -1
  140. package/dist/plugins/session-rules-column.d.ts +1 -1
  141. package/dist/plugins/session-rules-column.js +1 -1
  142. package/dist/plugins/session-workspace-cell.d.ts +1 -1
  143. package/dist/plugins/session-workspace-cell.js +1 -1
  144. package/dist/plugins/shared-memory-store-sql.js +2 -2
  145. package/dist/plugins/sql-driver.d.ts +10 -5
  146. package/dist/plugins/sql-driver.js +6 -6
  147. package/dist/plugins/sql-errors.js +1 -1
  148. package/dist/plugins/store-backend.d.ts +4 -6
  149. package/dist/plugins/store-backend.js +6 -6
  150. package/dist/plugins/task-list-store-sql.js +11 -11
  151. package/dist/plugins/tidb-pool.js +5 -5
  152. package/dist/plugins/tidb-session-storage.js +4 -4
  153. package/dist/plugins/tidb-session-store.js +13 -13
  154. package/dist/plugins/tool-result-store-sql.d.ts +2 -2
  155. package/dist/plugins/tool-result-store-sql.js +3 -3
  156. package/dist/plugins/user-input-ledger-sql.js +2 -2
  157. package/dist/plugins/workflow-journal-store-sql.js +5 -5
  158. package/dist/plugins/workflow-run-store-sql.js +19 -19
  159. package/dist/plugins/write-behind-counter.d.ts +2 -3
  160. package/dist/plugins/write-behind-counter.js +5 -5
  161. package/dist/resource-suspend.d.ts +14 -12
  162. package/dist/resource-suspend.js +1 -1
  163. package/dist/resource-window.d.ts +4 -3
  164. package/dist/run-local.js +14 -19
  165. package/dist/runs.d.ts +8 -4
  166. package/dist/runs.js +49 -65
  167. package/dist/session-incarnation.d.ts +1 -1
  168. package/dist/session-incarnation.js +1 -1
  169. package/dist/session-policy-wire.d.ts +1 -1
  170. package/dist/sql-engine-posture.d.ts +1 -1
  171. package/dist/sql-engine-posture.js +1 -1
  172. package/dist/{plugins → stores-sql}/background-agent-store-sql.d.ts +2 -2
  173. package/dist/{plugins → stores-sql}/background-agent-store-sql.js +8 -8
  174. package/dist/{plugins → stores-sql}/session-policy-store-sql.d.ts +7 -5
  175. package/dist/{plugins → stores-sql}/session-policy-store-sql.js +12 -12
  176. package/dist/{plugins → stores-sql}/usage-window-store-sql.d.ts +1 -1
  177. package/dist/{plugins → stores-sql}/usage-window-store-sql.js +3 -3
  178. package/dist/task-a2a.js +5 -6
  179. package/dist/task-mcp.d.ts +20 -7
  180. package/dist/task-mcp.js +17 -9
  181. package/dist/terminal.d.ts +27 -0
  182. package/dist/terminal.js +6 -0
  183. package/dist/trace/redact.d.ts +3 -1
  184. package/dist/trace/redact.js +80 -28
  185. package/dist/trace/self-minted-wire.d.ts +58 -0
  186. package/dist/trace/self-minted-wire.js +16 -0
  187. package/dist/web-admin-client/candidate-gate.d.ts +11 -7
  188. package/dist/web-admin-client/candidate-gate.js +1 -1
  189. package/dist/web-admin-client/http-client.d.ts +5 -4
  190. package/dist/web-admin-client/http-client.js +2 -2
  191. package/dist/web-admin-client/read-warnings.d.ts +7 -17
  192. package/dist/web-admin-client/read-warnings.js +5 -9
  193. package/package.json +2 -2
  194. package/wire-changes/server@7.140.0.json +184 -0
@@ -410,7 +410,7 @@ function refuseRowsTheValidatorRefuses(ownerKey, rules, tombstones) {
410
410
  }
411
411
  }
412
412
  function fencedCasWhere(dialect, firstPlaceholder, ownerKey, cas) {
413
- const ph = (offset) => (dialect === "tidb" ? "?" : `$${firstPlaceholder + offset}`);
413
+ const ph = (offset) => (dialect === "mysql" ? "?" : `$${firstPlaceholder + offset}`);
414
414
  return {
415
415
  sql: `owner_key = ${ph(0)} AND ${cas.column} = ${ph(1)} AND (grammar IS NULL OR grammar <= ${ph(2)})`,
416
416
  params: [ownerKey, cas.expected, SESSION_RULES_GRAMMAR],
@@ -430,7 +430,7 @@ async function writeBucket(exec, dialect, ownerKey, cur, next, nowMs) {
430
430
  const body = { rules: next.rules, tombstones: next.tombstones, quarantined: cur.quarantined, ...(cur.observedVector !== undefined ? { observedVector: cur.observedVector } : {}) };
431
431
  const enc = dialectProtocolJsonEncoder(dialect);
432
432
  const where = fencedCasWhere(dialect, 7, ownerKey, { column: "rev", expected: cur.rev });
433
- const affected = await exec(dialect === "tidb"
433
+ const affected = await exec(dialect === "mysql"
434
434
  ? `UPDATE ${PERMISSION_RULE_TABLE} SET rules_json = ?, tombstones_json = ?, quarantined_json = ?, checksum = ?, grammar = ?, rev = rev + 1, updated_at_ms = ? WHERE ${where.sql}`
435
435
  : `UPDATE ${PERMISSION_RULE_TABLE} SET rules_json = $1, tombstones_json = $2, quarantined_json = $3, checksum = $4, grammar = $5, rev = rev + 1, updated_at_ms = $6 WHERE ${where.sql}`, [
436
436
  enc(next.rules, "permission_rule.rules_json"),
@@ -460,13 +460,13 @@ class SqlDurableRulePartition {
460
460
  return dialectProtocolJsonEncoder(this.db.dialect)(v, label);
461
461
  }
462
462
  q(tidbSql, pgSql) {
463
- return this.db.dialect === "tidb" ? tidbSql : pgSql;
463
+ return this.db.dialect === "mysql" ? tidbSql : pgSql;
464
464
  }
465
465
  async read() {
466
466
  return refusingUnreadableColumns(`bucket ${this.ownerKey}`, () => this.readBucketRow());
467
467
  }
468
468
  async readBucketRow() {
469
- const { rows } = await this.db.query(this.q(`SELECT ${bucketRowColumns("tidb")} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = ?`, `SELECT ${bucketRowColumns("pg")} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = $1`), [this.ownerKey]);
469
+ const { rows } = await this.db.query(this.q(`SELECT ${bucketRowColumns("mysql")} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = ?`, `SELECT ${bucketRowColumns("pg")} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = $1`), [this.ownerKey]);
470
470
  const row = rows[0];
471
471
  if (row === undefined)
472
472
  return { ...EMPTY_BUCKET, rules: [], tombstones: [], quarantined: [] };
@@ -604,7 +604,7 @@ export class SqlRuleApprovalRecordStore {
604
604
  this.now = now;
605
605
  }
606
606
  q(tidbSql, pgSql) {
607
- return this.db.dialect === "tidb" ? tidbSql : pgSql;
607
+ return this.db.dialect === "mysql" ? tidbSql : pgSql;
608
608
  }
609
609
  enc(v, label) {
610
610
  return dialectProtocolJsonEncoder(this.db.dialect)(v, label);
@@ -613,7 +613,7 @@ export class SqlRuleApprovalRecordStore {
613
613
  return refusingUnreadableColumns(`approval record ${id}`, () => this.readApprovalRow(id));
614
614
  }
615
615
  async readApprovalRow(id) {
616
- const { rows } = await this.db.query(this.q(`SELECT record_id, owner_kind, principal, kind, state, version, rev, ${jsonColumnText("tidb", "candidates_json")}, ${jsonColumnText("tidb", "offers_json")}, selected_offer, ${jsonColumnText("tidb", "redeemed_dots_json")}, tool_call_id, bound_input_hash, command, ${jsonColumnText("tidb", "edited_json")}, created_at_iso FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE record_id = ?`, `SELECT record_id, owner_kind, principal, kind, state, version, rev, ${jsonColumnText("pg", "candidates_json")}, ${jsonColumnText("pg", "offers_json")}, selected_offer, ${jsonColumnText("pg", "redeemed_dots_json")}, tool_call_id, bound_input_hash, command, ${jsonColumnText("pg", "edited_json")}, created_at_iso FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE record_id = $1`), [id]);
616
+ const { rows } = await this.db.query(this.q(`SELECT record_id, owner_kind, principal, kind, state, version, rev, ${jsonColumnText("mysql", "candidates_json")}, ${jsonColumnText("mysql", "offers_json")}, selected_offer, ${jsonColumnText("mysql", "redeemed_dots_json")}, tool_call_id, bound_input_hash, command, ${jsonColumnText("mysql", "edited_json")}, created_at_iso FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE record_id = ?`, `SELECT record_id, owner_kind, principal, kind, state, version, rev, ${jsonColumnText("pg", "candidates_json")}, ${jsonColumnText("pg", "offers_json")}, selected_offer, ${jsonColumnText("pg", "redeemed_dots_json")}, tool_call_id, bound_input_hash, command, ${jsonColumnText("pg", "edited_json")}, created_at_iso FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE record_id = $1`), [id]);
617
617
  const row = rows[0];
618
618
  if (row === undefined)
619
619
  return undefined;
@@ -717,7 +717,7 @@ export class SqlRuleImportTicketStore {
717
717
  this.db = db;
718
718
  }
719
719
  q(tidbSql, pgSql) {
720
- return this.db.dialect === "tidb" ? tidbSql : pgSql;
720
+ return this.db.dialect === "mysql" ? tidbSql : pgSql;
721
721
  }
722
722
  async dbNowMs() {
723
723
  return sqlDbNowMs(this.db, PERMISSION_RULE_TICKET_TABLE);
@@ -755,7 +755,7 @@ export class SqlRuleImportTicketStore {
755
755
  if (row.expiresAtMs <= row.dbNowMs)
756
756
  return { ok: false, reason: "expired" };
757
757
  const claimId = randomUUID();
758
- const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.tidb}, claim_id = ? WHERE ticket_id = ? AND owner_key = ? AND purpose = ? AND consumed_at_ms IS NULL AND expires_at_ms > ${SQL_DB_NOW_MS.tidb}`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.pg}, claim_id = $1 WHERE ticket_id = $2 AND owner_key = $3 AND purpose = $4 AND consumed_at_ms IS NULL AND expires_at_ms > ${SQL_DB_NOW_MS.pg}`), [claimId, ticketId, ownerKey, purpose]);
758
+ const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.mysql}, claim_id = ? WHERE ticket_id = ? AND owner_key = ? AND purpose = ? AND consumed_at_ms IS NULL AND expires_at_ms > ${SQL_DB_NOW_MS.mysql}`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.pg}, claim_id = $1 WHERE ticket_id = $2 AND owner_key = $3 AND purpose = $4 AND consumed_at_ms IS NULL AND expires_at_ms > ${SQL_DB_NOW_MS.pg}`), [claimId, ticketId, ownerKey, purpose]);
759
759
  if (affected === 1)
760
760
  return { ok: true, approvalId: row.approvalId, payloadHash: row.payloadHash, claimId };
761
761
  const after = await this.readRow(ticketId);
@@ -768,11 +768,11 @@ export class SqlRuleImportTicketStore {
768
768
  return { ok: false, reason: "consumed" };
769
769
  }
770
770
  async release(ticketId, principal, claimId, mustRemainValidMs = 0) {
771
- const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = NULL, claim_id = NULL WHERE ticket_id = ? AND owner_key = ? AND claim_id = ? AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND expires_at_ms > ${SQL_DB_NOW_MS.tidb} + ?`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = NULL, claim_id = NULL WHERE ticket_id = $1 AND owner_key = $2 AND claim_id = $3 AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND expires_at_ms > ${SQL_DB_NOW_MS.pg} + $4`), [ticketId, buildRuleOwnerKey({ kind: "principal", principal }), claimId, mustRemainValidMs]);
771
+ const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = NULL, claim_id = NULL WHERE ticket_id = ? AND owner_key = ? AND claim_id = ? AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND expires_at_ms > ${SQL_DB_NOW_MS.mysql} + ?`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = NULL, claim_id = NULL WHERE ticket_id = $1 AND owner_key = $2 AND claim_id = $3 AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND expires_at_ms > ${SQL_DB_NOW_MS.pg} + $4`), [ticketId, buildRuleOwnerKey({ kind: "principal", principal }), claimId, mustRemainValidMs]);
772
772
  return affected === 1;
773
773
  }
774
774
  async settle(ticketId, principal, claimId, outcome) {
775
- const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET redeemed_outcome = ?, settled_at_ms = ${SQL_DB_NOW_MS.tidb} WHERE ticket_id = ? AND owner_key = ? AND claim_id = ? AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET redeemed_outcome = $1, settled_at_ms = ${SQL_DB_NOW_MS.pg} WHERE ticket_id = $2 AND owner_key = $3 AND claim_id = $4 AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL`), [
775
+ const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET redeemed_outcome = ?, settled_at_ms = ${SQL_DB_NOW_MS.mysql} WHERE ticket_id = ? AND owner_key = ? AND claim_id = ? AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET redeemed_outcome = $1, settled_at_ms = ${SQL_DB_NOW_MS.pg} WHERE ticket_id = $2 AND owner_key = $3 AND claim_id = $4 AND consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL`), [
776
776
  dialectProtocolJsonEncoder(this.db.dialect)(outcome, "permission_rule_ticket.redeemed_outcome"),
777
777
  ticketId,
778
778
  buildRuleOwnerKey({ kind: "principal", principal }),
@@ -805,11 +805,11 @@ export class SqlRuleImportTicketStore {
805
805
  if (row.claimId === undefined)
806
806
  return { ok: false };
807
807
  const claimId = randomUUID();
808
- const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.tidb}, claim_id = ? WHERE ticket_id = ? AND owner_key = ? AND purpose = ? AND claim_id = ? AND consumed_at_ms IS NOT NULL AND consumed_at_ms <= ${SQL_DB_NOW_MS.tidb} - ? AND redeemed_outcome IS NULL`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.pg}, claim_id = $1 WHERE ticket_id = $2 AND owner_key = $3 AND purpose = $4 AND claim_id = $5 AND consumed_at_ms IS NOT NULL AND consumed_at_ms <= ${SQL_DB_NOW_MS.pg} - $6 AND redeemed_outcome IS NULL`), [claimId, ticketId, buildRuleOwnerKey({ kind: "principal", principal }), purpose, row.claimId, minClaimAgeMs]);
808
+ const { affected } = await this.db.query(this.q(`UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.mysql}, claim_id = ? WHERE ticket_id = ? AND owner_key = ? AND purpose = ? AND claim_id = ? AND consumed_at_ms IS NOT NULL AND consumed_at_ms <= ${SQL_DB_NOW_MS.mysql} - ? AND redeemed_outcome IS NULL`, `UPDATE ${PERMISSION_RULE_TICKET_TABLE} SET consumed_at_ms = ${SQL_DB_NOW_MS.pg}, claim_id = $1 WHERE ticket_id = $2 AND owner_key = $3 AND purpose = $4 AND claim_id = $5 AND consumed_at_ms IS NOT NULL AND consumed_at_ms <= ${SQL_DB_NOW_MS.pg} - $6 AND redeemed_outcome IS NULL`), [claimId, ticketId, buildRuleOwnerKey({ kind: "principal", principal }), purpose, row.claimId, minClaimAgeMs]);
809
809
  return affected === 1 ? { ok: true, approvalId: row.approvalId, payloadHash: row.payloadHash, claimId } : { ok: false };
810
810
  }
811
811
  async readRow(ticketId) {
812
- const { rows } = await this.db.query(this.q(`SELECT owner_key, approval_id, payload_hash, purpose, expires_at_ms, consumed_at_ms, claim_id, ${jsonColumnText("tidb", "redeemed_outcome")}, ${SQL_DB_NOW_MS.tidb} AS db_now_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ticket_id = ?`, `SELECT owner_key, approval_id, payload_hash, purpose, expires_at_ms, consumed_at_ms, claim_id, ${jsonColumnText("pg", "redeemed_outcome")}, ${SQL_DB_NOW_MS.pg} AS db_now_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ticket_id = $1`), [ticketId]);
812
+ const { rows } = await this.db.query(this.q(`SELECT owner_key, approval_id, payload_hash, purpose, expires_at_ms, consumed_at_ms, claim_id, ${jsonColumnText("mysql", "redeemed_outcome")}, ${SQL_DB_NOW_MS.mysql} AS db_now_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ticket_id = ?`, `SELECT owner_key, approval_id, payload_hash, purpose, expires_at_ms, consumed_at_ms, claim_id, ${jsonColumnText("pg", "redeemed_outcome")}, ${SQL_DB_NOW_MS.pg} AS db_now_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ticket_id = $1`), [ticketId]);
813
813
  const row = rows[0];
814
814
  if (row === undefined)
815
815
  return undefined;
@@ -834,7 +834,7 @@ export async function purgeUndurableScopeRows(db, dialect, into, now = Date.now)
834
834
  const cols = `owner_key, ${bucketRowColumns(dialect)}`;
835
835
  let after = "";
836
836
  for (;;) {
837
- const page = await db.rows(dialect === "tidb"
837
+ const page = await db.rows(dialect === "mysql"
838
838
  ? `SELECT ${cols} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key > ? ORDER BY owner_key LIMIT ${UNDURABLE_PURGE_PAGE}`
839
839
  : `SELECT ${cols} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key > $1 ORDER BY owner_key LIMIT ${UNDURABLE_PURGE_PAGE}`, [after]);
840
840
  for (const first of page) {
@@ -857,7 +857,7 @@ export async function purgeUndurableScopeRows(db, dialect, into, now = Date.now)
857
857
  settled = true;
858
858
  break;
859
859
  case "conflict":
860
- row = (await db.rows(dialect === "tidb" ? `SELECT ${cols} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = ?` : `SELECT ${cols} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = $1`, [ownerKey]))[0];
860
+ row = (await db.rows(dialect === "mysql" ? `SELECT ${cols} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = ?` : `SELECT ${cols} FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = $1`, [ownerKey]))[0];
861
861
  if (row === undefined)
862
862
  settled = true;
863
863
  break;
@@ -907,13 +907,13 @@ function contentFreeRefusal(ownerKey, e) {
907
907
  return `permission_rule bucket ${ownerKey}: the bucket judge threw ${e instanceof Error ? e.name : typeof e} (its text is withheld here: it may quote the row)`;
908
908
  }
909
909
  export function createSqlPermissionRuleStores(db, now) {
910
- const q = (tidb, pg) => (db.dialect === "tidb" ? tidb : pg);
910
+ const q = (mysql, pg) => (db.dialect === "mysql" ? mysql : pg);
911
911
  return {
912
912
  durable: new SqlDurableRulePartitionProvider(db, now),
913
913
  approvals: new SqlRuleApprovalRecordStore(db, now),
914
914
  tickets: new SqlRuleImportTicketStore(db),
915
915
  reapExpired: async (nowMs) => {
916
- const deadTickets = await db.query(q(`DELETE FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE (consumed_at_ms IS NULL AND expires_at_ms <= ${SQL_DB_NOW_MS.tidb} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (redeemed_outcome IS NOT NULL AND settled_at_ms IS NOT NULL AND settled_at_ms <= ${SQL_DB_NOW_MS.tidb} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND consumed_at_ms < ${SQL_DB_NOW_MS.tidb} - ${RULE_PENDING_APPROVAL_RETENTION_MS}) LIMIT ${RULE_REAP_BATCH}`, `DELETE FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ctid IN (SELECT ctid FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE (consumed_at_ms IS NULL AND expires_at_ms <= ${SQL_DB_NOW_MS.pg} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (redeemed_outcome IS NOT NULL AND settled_at_ms IS NOT NULL AND settled_at_ms <= ${SQL_DB_NOW_MS.pg} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND consumed_at_ms < ${SQL_DB_NOW_MS.pg} - ${RULE_PENDING_APPROVAL_RETENTION_MS}) LIMIT ${RULE_REAP_BATCH})`), []);
916
+ const deadTickets = await db.query(q(`DELETE FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE (consumed_at_ms IS NULL AND expires_at_ms <= ${SQL_DB_NOW_MS.mysql} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (redeemed_outcome IS NOT NULL AND settled_at_ms IS NOT NULL AND settled_at_ms <= ${SQL_DB_NOW_MS.mysql} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND consumed_at_ms < ${SQL_DB_NOW_MS.mysql} - ${RULE_PENDING_APPROVAL_RETENTION_MS}) LIMIT ${RULE_REAP_BATCH}`, `DELETE FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ctid IN (SELECT ctid FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE (consumed_at_ms IS NULL AND expires_at_ms <= ${SQL_DB_NOW_MS.pg} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (redeemed_outcome IS NOT NULL AND settled_at_ms IS NOT NULL AND settled_at_ms <= ${SQL_DB_NOW_MS.pg} - ${RULE_IMPORT_TICKET_GC_GRACE_MS}) OR (consumed_at_ms IS NOT NULL AND redeemed_outcome IS NULL AND consumed_at_ms < ${SQL_DB_NOW_MS.pg} - ${RULE_PENDING_APPROVAL_RETENTION_MS}) LIMIT ${RULE_REAP_BATCH})`), []);
917
917
  const orphanPending = await db.query(q(`DELETE FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE state = 'pending' AND created_at_ms < ? AND NOT EXISTS (SELECT 1 FROM ${PERMISSION_RULE_TICKET_TABLE} t WHERE t.approval_id = ${PERMISSION_RULE_APPROVAL_TABLE}.record_id) LIMIT ${RULE_REAP_BATCH}`, `DELETE FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE ctid IN (SELECT a.ctid FROM ${PERMISSION_RULE_APPROVAL_TABLE} a WHERE a.state = 'pending' AND a.created_at_ms < $1 AND NOT EXISTS (SELECT 1 FROM ${PERMISSION_RULE_TICKET_TABLE} t WHERE t.approval_id = a.record_id) LIMIT ${RULE_REAP_BATCH})`), [nowMs - RULE_PENDING_APPROVAL_RETENTION_MS]);
918
918
  return deadTickets.affected + orphanPending.affected;
919
919
  },
@@ -52,28 +52,8 @@ export interface AdbEnvDeps {
52
52
  * **结构**信号;adb 的 shell v2 没有等价物,所以这里只能靠标记词,**这条臂就把话说在这里**。 */
53
53
  export declare function isAdbTransportLost(stderr: string): boolean;
54
54
  export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
55
- /**
56
- * 🔴 **[ref] / core 7.20.0([ref])`canonicalPathAuthoritative: true`** —— 本适配器的 `canonicalPath`
57
- * 就是**命令真正跑的那块文件系统**的权威答案(`posix-shell-fs` 的 `canonicalPath` 原语,在设备 shell 里解到落点)。声明它 = 告诉引擎:整链答不出来是**解析器出了事**
58
- * (RPC 断、传输掉),不是「一个它看不见的命名空间」⇒ 读边界的执行期复核**拒**,而不是让词法判决站着。
59
- * 缺席是今天每个适配器的读法(整链沉默 ⇒ 词法判决成立、命令照跑),所以这一声明是**收紧**方向。
60
- * 严格读法 `=== true`(core `canonicalPathIsAuthoritative`);`isSuspendable` 一族同样严格,对本仓的
61
- * 布尔字面量零影响(四键都写死字面 `true`/`false`,不存在「非布尔值被读成缺席」那一形 —— 那一形由
62
- * core 的 `config.execution_env_capability_invalid` 普查响亮播报,本仓的受众表已登记)。
63
- * 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
64
- */
65
- /** `sharesProcessTable`(core 7.57.0,必填):命令跑在设备上(adb shell),不在本进程所在的进程表。 */
66
- /** 能力块(引擎 [ref],八键必填、各键只经引擎读器读):`hostLocalPaths` = 这条车道答出的路径是不是本进程所在机器上的文件;`externalContentTarget` = 产出是否来自部署信任边界之外;`io` = 文件与 shell 两面真到达目标;`keepsReadFloor` = 是否守住读拒表的字节地板(本车道不守 ⇒ 读拒表非空的 run 被告知,部署要求地板时准备期拒)。 */
67
- readonly capabilities: {
68
- isolation: false;
69
- suspendable: false;
70
- canonicalPathAuthoritative: true;
71
- sharesProcessTable: false;
72
- hostLocalPaths: false;
73
- externalContentTarget: false;
74
- io: true;
75
- keepsReadFloor: false;
76
- };
55
+ /** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。 */
56
+ readonly capabilities: import("@sema-agent/core").ExecutionEnvCapabilities;
77
57
  cwd: string;
78
58
  private connected;
79
59
  /** 🔴 拆除是**终态**(2026-07-25 补齐:此前本适配器是同族里唯一没有这个概念的一条腿)。
@@ -85,10 +65,13 @@ export declare class RemoteAdbExecutionEnv implements RemoteExecutionEnv {
85
65
  private destroyed;
86
66
  /** 在飞的那一次懒连接(单飞记忆)——见 {@link ensureConnected}。settle 即清,**不缓存败诺**。 */
87
67
  private connectPromise?;
88
- /** core `ExecutionEnv.homeDir`([ref]):设备 shell 里执行用户的 home —— 声明胜,否则首连时 `adb shell` 实测的 `$HOME`(Android 常为 `/` 或 `/data/local/tmp` 一类,读到什么填什么,绝对即可),见 {@link LaneHomeSeat}。读时才取(没声明时连上之前缺席)。 */
68
+ /** core `ExecutionEnv.homeDir`([ref]):设备 shell 里执行用户的 home —— 声明在场 ⇒ 声明(首连时与设备 shell 的 `$HOME` 对证过),否则首连时 `adb shell` 实测的 `$HOME`(Android 常为 `/` 或 `/data/local/tmp` 一类,读到什么填什么,绝对即可),见 {@link LaneHomeSeat}。读时才取(没声明时连上之前缺席)。 */
89
69
  get homeDir(): string | undefined;
90
- /** 家目录的座(声明胜 · 否则首连时实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。本车道**不对证**声明:Android 没有 POSIX 登录家目录,声明是运维的断言,照单全收。 */
70
+ /** 家目录的座(声明在场 ⇒ 首连时对证 · 否则首连时实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。设备 shell 答得出 `$HOME`,命令里的 `~` / `$HOME` 展开的就是它 ——
71
+ * 声明与它不一致时规则守的是声明那个目录,所以与其余读得出家目录的车道同一只判官、同一句拒。 */
91
72
  private readonly homeSeat;
73
+ /** 家目录:设备答了而与声明不一致(或答的不是绝对路径)⇒ 这只环境的拒,记一次、此后每次连接原样回它(同一台设备同一个答案:不重试、不再问)。 */
74
+ private homeRefusal?;
92
75
  /** core 7.49.0 [ref]:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在 Android 设备的 shell 里(设备自己的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
93
76
  * 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
94
77
  readonly cdPath: "unknown";
@@ -9,6 +9,7 @@ import { signalNumber } from "./host-platform.js";
9
9
  import { buildLivenessCut, commandBoundsOf } from "../exec-bounds.js";
10
10
  import { buildCapturedOutput, createCutError } from "../exec-cut.js";
11
11
  import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
12
+ import { capabilitiesFor } from "../execution-lane-caps.js";
12
13
  import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
13
14
  import { createPosixShellFs } from "./posix-shell-fs.js";
14
15
  const PROVIDER = "adb";
@@ -21,14 +22,14 @@ function lazyConnectExecError(e) {
21
22
  return e.code === "aborted" ? new ExecutionError("aborted", e.message, e) : new ExecutionError("shell_unavailable", `ADB lazy-connect failed: ${e.message}`, e);
22
23
  }
23
24
  function lazyConnectFileError(e, absPath) {
24
- return e.code === "aborted" ? new FileError("aborted", e.message, absPath, e) : new FileError("unknown", `ADB lazy-connect failed: ${e.message}`, absPath, e);
25
+ return fileErrorFromExec(e, absPath);
25
26
  }
26
27
  const isTcp = (serial) => /:\d+$/.test(serial);
27
28
  export function isAdbTransportLost(stderr) {
28
29
  return /device offline|device '.*' not found|no devices\/emulators found|connection reset|protocol fault|device disconnected/i.test(stderr);
29
30
  }
30
31
  export class RemoteAdbExecutionEnv {
31
- capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false };
32
+ capabilities = capabilitiesFor({ remoteExec: { provider: "adb" } });
32
33
  cwd;
33
34
  connected = false;
34
35
  destroyed = false;
@@ -37,6 +38,7 @@ export class RemoteAdbExecutionEnv {
37
38
  return this.homeSeat.home;
38
39
  }
39
40
  homeSeat;
41
+ homeRefusal;
40
42
  cdPath = "unknown";
41
43
  withheldEnv = [];
42
44
  cfg;
@@ -52,7 +54,7 @@ export class RemoteAdbExecutionEnv {
52
54
  retry: { maxAttempts: config.retry?.maxAttempts ?? 3, backoffMs: config.retry?.backoffMs ?? 800 },
53
55
  };
54
56
  this.cwd = this.cfg.mountPath;
55
- this.homeSeat = new LaneHomeSeat({ lane: PROVIDER, declared: config.homeDir, verifiesDeclared: false, onAbsent: config.onHomeAbsent });
57
+ this.homeSeat = new LaneHomeSeat(PROVIDER, config);
56
58
  this.spawnFn = deps?.spawn ?? spawn;
57
59
  this.injectedConnectOnce = deps?.connectOnce;
58
60
  }
@@ -65,13 +67,22 @@ export class RemoteAdbExecutionEnv {
65
67
  async connect(config) {
66
68
  if (this.destroyed)
67
69
  return { ok: false, error: new RemoteExecutionError("connect_failed", "ADB env destroyed — create a fresh env instead of reviving") };
70
+ if (this.homeRefusal)
71
+ return { ok: false, error: this.homeRefusal };
68
72
  const policy = {
69
73
  retryableCodes: ["auth_transient", "connect_failed", "timeout"],
70
74
  maxAttempts: this.cfg.retry.maxAttempts,
71
75
  backoffMs: (failed) => this.cfg.retry.backoffMs * failed,
72
76
  };
73
77
  const once = this.injectedConnectOnce ?? ((s) => this.connectOnce(s));
74
- return withRetry((_attempt) => once(config?.abortSignal), policy, { signal: config?.abortSignal });
78
+ const refused = new AbortController();
79
+ const halt = config?.abortSignal ? AbortSignal.any([config.abortSignal, refused.signal]) : refused.signal;
80
+ return withRetry(async () => {
81
+ const r = await once(config?.abortSignal);
82
+ if (!r.ok && this.homeRefusal)
83
+ refused.abort();
84
+ return r;
85
+ }, policy, { signal: halt });
75
86
  }
76
87
  async ensureConnected() {
77
88
  if (this.destroyed)
@@ -127,7 +138,13 @@ export class RemoteAdbExecutionEnv {
127
138
  const home = await this.adb(this.deviceShellArgs(LANE_HOME_PROBE_COMMAND), { signal, timeoutMs: this.cfg.commandTimeoutMs });
128
139
  if (!home.ok && home.error.code === "aborted")
129
140
  return { ok: false, error: new RemoteExecutionError("aborted", "connect aborted while reading the device's home") };
130
- this.homeSeat.settle(home.ok && home.value.exitCode === 0 ? { ok: true, stdout: home.value.stdout } : { ok: false, reason: home.ok ? `exit ${home.value.exitCode}` : home.error.message });
141
+ const verdict = this.homeSeat.settle(home.ok && home.value.exitCode === 0 ? { ok: true, stdout: home.value.stdout } : { ok: false, reason: home.ok ? `exit ${home.value.exitCode}` : home.error.message });
142
+ if (verdict !== undefined) {
143
+ const error = new RemoteExecutionError("connect_failed", verdict.refusal);
144
+ if (verdict.answered)
145
+ this.homeRefusal = error;
146
+ return { ok: false, error };
147
+ }
131
148
  }
132
149
  this.connected = true;
133
150
  return ok(this.workspaceHandle());
@@ -95,34 +95,8 @@ export declare class RemoteDeviceExecutionEnv implements RemoteExecutionEnv {
95
95
  /** core 7.56.0 [ref]:shell 子进程继承的环境里收不到的条目 —— 本车道 shell 的环境在设备上**构建**(本进程的 env 从不注入,只有引擎逐次交的 `env`
96
96
  * 这条显式授予),没有一条是从本进程继承的 ⇒ `[]`(core 契约:「`[]` when its shell environment is built rather than inherited」)。 */
97
97
  readonly withheldEnv: readonly [];
98
- /**
99
- * 🔴 `suspendable: false` 在 core 契约里**同时断言**「工作区在目标上外部持久」(remote-env.ts 的
100
- * load-bearing 不变量)—— 设备工作区 = 员工机磁盘,真持久 ✅,park-only 降级腿因此合法可用。
101
- * 谁要把 executor 的工作区改成临时目录,先来改这一行:那一刻这个 `false` 就变成谎话,park 赎回会
102
- * 落到一个空工作区上而链路上没有任何一层会报错。
103
- */
104
- /**
105
- * 🔴 **[ref] / core 7.20.0([ref])`canonicalPathAuthoritative: true`** —— 本适配器的 `canonicalPath`
106
- * 就是**命令真正跑的那块文件系统**的权威答案(设备 WS 协议的**必答** `canonicalPath` op)。声明它 = 告诉引擎:整链答不出来是**解析器出了事**
107
- * (RPC 断、传输掉),不是「一个它看不见的命名空间」⇒ 读边界的执行期复核**拒**,而不是让词法判决站着。
108
- * 缺席是今天每个适配器的读法(整链沉默 ⇒ 词法判决成立、命令照跑),所以这一声明是**收紧**方向。
109
- * 严格读法 `=== true`(core `canonicalPathIsAuthoritative`);`isSuspendable` 一族同样严格,对本仓的
110
- * 布尔字面量零影响(四键都写死字面 `true`/`false`,不存在「非布尔值被读成缺席」那一形 —— 那一形由
111
- * core 的 `config.execution_env_capability_invalid` 普查响亮播报,本仓的受众表已登记)。
112
- * 谁该声明由闭集门看着(`test/execution-lane-caps.test.ts` 的逐 lane 对账:新 lane 不表态 = 编译红)。
113
- */
114
- /** `sharesProcessTable`(core 7.57.0,必填):命令跑在注册设备上(设备执行器),不在本进程所在的进程表。 */
115
- /** 能力块(引擎 [ref],八键必填、各键只经引擎读器读):`hostLocalPaths` = 这条车道答出的路径是不是本进程所在机器上的文件;`externalContentTarget` = 产出是否来自部署信任边界之外;`io` = 文件与 shell 两面真到达目标;`keepsReadFloor` = 是否守住读拒表的字节地板(本车道不守 ⇒ 读拒表非空的 run 被告知,部署要求地板时准备期拒)。 */
116
- readonly capabilities: {
117
- isolation: false;
118
- suspendable: false;
119
- canonicalPathAuthoritative: true;
120
- sharesProcessTable: false;
121
- hostLocalPaths: false;
122
- externalContentTarget: true;
123
- io: true;
124
- keepsReadFloor: false;
125
- };
98
+ /** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。 */
99
+ readonly capabilities: import("@sema-agent/core").ExecutionEnvCapabilities;
126
100
  cwd: string;
127
101
  /** 可选面(presence-typed,见文件头「铁律」段):构造期按设备声明装配,缺席就是缺席。 */
128
102
  readLink?: (path: string, abortSignal?: AbortSignal) => Promise<Result<string, FileError>>;
@@ -1,5 +1,6 @@
1
1
  import { posix } from "node:path";
2
2
  import { ExecutionError, FileError, RemoteExecutionError, } from "@sema-agent/core";
3
+ import { capabilitiesFor } from "../execution-lane-caps.js";
3
4
  import { DEVICE_ERROR_CODES, isDeviceAbsolutePath, } from "../device-store.js";
4
5
  import { boundDeviceOf } from "../execution-placement.js";
5
6
  import { createCutError, createExecFailure } from "../exec-cut.js";
@@ -128,7 +129,7 @@ export class RemoteDeviceExecutionEnv {
128
129
  homeDir;
129
130
  cdPath = "unknown";
130
131
  withheldEnv = [];
131
- capabilities = { isolation: false, suspendable: false, canonicalPathAuthoritative: true, sharesProcessTable: false, hostLocalPaths: false, externalContentTarget: true, io: true, keepsReadFloor: false };
132
+ capabilities = capabilitiesFor({ remoteExec: { provider: "device" } });
132
133
  cwd;
133
134
  readLink;
134
135
  writeFileExclusive;
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import { Sandbox, type SandboxOpts, type SandboxConnectOpts } from "e2b";
23
23
  import { FileError, ExecutionError, RemoteExecutionError, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type ExecutionEnvFactory, type VmLifecycleOptions, type BackgroundShellCapability, type BackgroundShellId, type BackgroundShellError, type BackgroundSpawnOptions, type BackgroundPoll } from "@sema-agent/core";
24
+ import { type LaneHomeConfig } from "../lane-home-probe.js";
24
25
  /** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */
25
26
  type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
26
27
  /** Minimal optional observability hooks (kept structural so the adapter stays standalone-testable). */
@@ -34,22 +35,13 @@ export interface RemoteEnvObservers {
34
35
  };
35
36
  }
36
37
  /** Construction config for {@link RemoteContainerExecutionEnv}. Secrets/resolved values are control-plane supplied. */
37
- export interface RemoteContainerEnvConfig extends RemoteEnvObservers {
38
+ export interface RemoteContainerEnvConfig extends RemoteEnvObservers, LaneHomeConfig {
38
39
  /** E2B API key. Resolved by the trusted control plane from env — never baked into an image or committed. */
39
40
  apiKey: string;
40
41
  /** E2B template id (image). Defaults to the E2B base template. */
41
42
  template?: string;
42
43
  /** Sandbox lifetime in ms (E2B billing window). Default {@link DEFAULT_TIMEOUT_MS}. */
43
44
  timeoutMs?: number;
44
- /**
45
- * [ref]④ / core [ref] —— 本执行环境的 **home 目录**(`ExecutionEnv.homeDir` 座,绝对路径,本环境自己的
46
- * 命名空间里)。`~/…` 形权限规则(`Edit(~/.ssh/**)`)对**跑在本 env 里的调用**就以它为基。
47
- * 缺席 ⇒ 本 env 声明「我没有 home」,core 对 `~/` 规则判 `unreadable`(fail-closed 的 ask),
48
- * 绝不回落引擎进程自己的 home(那在远端腿上是另一个用户的另一个目录 —— 静默守错目录正是 [ref] 的病)。
49
- * 值由 `boot/execution-env.ts` 从**唯一属主** `execution-lane-caps.ts` 的 `executionLaneHomeDir` 递进来;
50
- * adapter 不自算(两处各算一份 = 422 门与引擎对同一条规则给相反答案)。
51
- */
52
- homeDir?: string;
53
45
  /** Workspace root inside the VM; remote paths normalize against it. Default {@link DEFAULT_MOUNT_PATH}. */
54
46
  mountPath?: string;
55
47
  /** Outbound network for the sandbox (clay: service needs external-network capability). Default `true`. */
@@ -99,8 +91,9 @@ export interface RemoteContainerEnvDeps {
99
91
  export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv, BackgroundShellCapability {
100
92
  /** Current working directory for relative paths (FileSystem contract; mutable, core may reassign). */
101
93
  cwd: string;
102
- /** core `ExecutionEnv.homeDir`([ref]):本沙箱/目标机上执行用户的 home。见配置同名字段。 */
103
- readonly homeDir?: string | undefined;
94
+ /** core `ExecutionEnv.homeDir`([ref]):沙箱里执行用户的 home —— 主张在场 ⇒ 主张(每只沙箱建起来 / 恢复时与沙箱里的 `$HOME` 对证过),
95
+ * 否则建沙箱那一拍实测的 `$HOME`,见 {@link LaneHomeSeat}。读时才取(没主张时沙箱建起来之前缺席)。 */
96
+ get homeDir(): string | undefined;
104
97
  /** core 7.49.0 [ref]:shell 子进程真拿到的 `CDPATH` 条目 —— 本车道的命令跑在 e2b 沙箱里(模板镜像的 env),本类不读那份 env ⇒ `"unknown"`(core 契约:说不出就说 unknown,
105
98
  * 不许报一张没读过的空表;删除守卫因此对裸 `cd <相对名>` 之后的删除问人,而不是按「没有 CDPATH」放行)。 */
106
99
  readonly cdPath: "unknown";
@@ -123,6 +116,10 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
123
116
  * and the post-await re-check in BOTH async-provision paths ({@link doConnect} and {@link doResumeVM}) against
124
117
  * a destroy that races an in-flight provision/restore. */
125
118
  private destroyed;
119
+ /** 家目录的座(主张在场 ⇒ 每只沙箱对证 · 否则建沙箱那一拍实测 · 实测缺席 ⇒ 缺席 + 一行 warn)。 */
120
+ private readonly homeSeat;
121
+ /** 家目录:沙箱答了而与主张不一致(或答的不是绝对路径)⇒ 这只环境的拒,记一次、此后每次连接 / 恢复原样回它(同一模板同一个答案:不再建沙箱、不再问)。 */
122
+ private homeRefusal?;
126
123
  /** In-flight connect/resume, memoized so concurrent lazy first-use OR resume provisions exactly one VM (no double-bill). */
127
124
  private connecting?;
128
125
  /** Count of exec/execStream calls in flight — `suspendVM` refuses while > 0 (council #4: never snapshot a running command). */
@@ -133,20 +130,8 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
133
130
  * SAME env instance skips the reconnect and every later fs/exec runs against the dead VM. */
134
131
  private paused;
135
132
  constructor(config: RemoteContainerEnvConfig, deps?: RemoteContainerEnvDeps);
136
- /** [ref] §9: E2B is a FULL RemoteExecutionEnv — an isolated, snapshot-suspendable microVM. (SSH/ADB
137
- * adapters declare {isolation:false, suspendable:false} — real machines/devices, not snapshotable.) */
138
- /** `sharesProcessTable`(core 7.57.0,必填):命令跑在云沙箱里,不在本进程所在的进程表。 */
139
- /** 能力块(引擎 [ref],八键必填、各键只经引擎读器读):`hostLocalPaths` = 这条车道答出的路径是不是本进程所在机器上的文件;`externalContentTarget` = 产出是否来自部署信任边界之外;`io` = 文件与 shell 两面真到达目标;`keepsReadFloor` = 是否守住读拒表的字节地板(本车道不守 ⇒ 读拒表非空的 run 被告知,部署要求地板时准备期拒)。 */
140
- readonly capabilities: {
141
- isolation: true;
142
- suspendable: true;
143
- canonicalPathAuthoritative: true;
144
- sharesProcessTable: false;
145
- hostLocalPaths: false;
146
- externalContentTarget: false;
147
- io: true;
148
- keepsReadFloor: false;
149
- };
133
+ /** 能力块的唯一作者 = `capabilitiesFor`(`execution-lane-caps.ts`;逐键表态与理由在那里,本类零能力字面量)。 */
134
+ readonly capabilities: import("@sema-agent/core").ExecutionEnvCapabilities;
150
135
  workspaceHandle(): WorkspaceHandle;
151
136
  /**
152
137
  * Public host for a port inside the sandbox (E2B reverse proxy → `<port>-<id>.e2b.app`). The LSP sidecar
@@ -217,6 +202,13 @@ export declare class RemoteContainerExecutionEnv implements RemoteExecutionEnv,
217
202
  ok: false;
218
203
  error: RemoteExecutionError;
219
204
  }>;
205
+ /**
206
+ * 家目录:座要问({@link LaneHomeSeat.asks}:主张在场 ⇒ 对证;没主张 ⇒ 量到绝对路径为止)⇒ 在这只沙箱里问一次 `$HOME`(前台 `commands.run`,与命令同一个
207
+ * 模板缺省用户、同一份沙箱 env),读数交座。返回这只沙箱的拒(`connect_failed`,拒句 = 同一只判官的句子),或 `undefined`(照用)。
208
+ * 沙箱答了的拒记在环境上(同一模板同一个答案);没答上(非零退出 / RPC 失败 / 输出里没有标记行)⇒ 这一次不开、不记,下一次重问;调用方中止 ⇒ `aborted`、不交座。
209
+ * 上界 = 控制面 RPC 的墙钟(`rpcTimeoutMs`:问一个环境变量,不是跑用户命令)。
210
+ */
211
+ private homeVerdict;
220
212
  /** BL-31 — (re)start the periodic sandbox-lifetime extension. `setTimeout(timeoutMs)` resets the auto-kill window
221
213
  * from now; we re-arm at ~80% of the (clamped, ≤1h) window so a long-held sandbox never expires mid-run. A no-op
222
214
  * when paused/destroyed; `unref`'d so it never keeps the process alive; cleared on destroy. */
@@ -1,6 +1,8 @@
1
1
  import path from "node:path";
2
2
  import { Sandbox, CommandExitError, FileType } from "e2b";
3
3
  import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
4
+ import { capabilitiesFor } from "../execution-lane-caps.js";
5
+ import { LANE_HOME_PROBE_COMMAND, LaneHomeSeat } from "../lane-home-probe.js";
4
6
  import { fileErrorFromExec } from "./remote-env-file-error.js";
5
7
  import { appendFileStaged, rewriteAppend, numEnvOr, runSymlinkChain, shellQuote, writeFileStaged } from "./remote-shell.js";
6
8
  import { buildLivenessCut, commandBoundsOf, firstBoundOf, laneLivenessOf, LIVENESS_MS_DEFAULT } from "../exec-bounds.js";
@@ -19,7 +21,9 @@ const E2B_BG_PROVIDER_BACKSTOP_PAD_SEC = 30;
19
21
  const ok = (value) => ({ ok: true, value });
20
22
  export class RemoteContainerExecutionEnv {
21
23
  cwd;
22
- homeDir;
24
+ get homeDir() {
25
+ return this.homeSeat.home;
26
+ }
23
27
  cdPath = "unknown";
24
28
  withheldEnv = [];
25
29
  cfg;
@@ -29,6 +33,8 @@ export class RemoteContainerExecutionEnv {
29
33
  keepAlive;
30
34
  handle;
31
35
  destroyed = false;
36
+ homeSeat;
37
+ homeRefusal;
32
38
  connecting;
33
39
  activeCommands = 0;
34
40
  paused = false;
@@ -44,10 +50,10 @@ export class RemoteContainerExecutionEnv {
44
50
  };
45
51
  this.laneLiveness = laneLivenessOf(this.cfg.livenessMs, "E2B_LIVENESS_MS");
46
52
  this.cwd = this.cfg.mountPath;
47
- this.homeDir = config.homeDir;
53
+ this.homeSeat = new LaneHomeSeat(PROVIDER, config);
48
54
  this.sandboxApi = deps.sandboxApi ?? Sandbox;
49
55
  }
50
- capabilities = { isolation: true, suspendable: true, canonicalPathAuthoritative: true, sharesProcessTable: false, hostLocalPaths: false, externalContentTarget: false, io: true, keepsReadFloor: false };
56
+ capabilities = capabilitiesFor({ remoteExec: { provider: "e2b" } });
51
57
  workspaceHandle() {
52
58
  if (!this.handle) {
53
59
  throw new RemoteExecutionError("connect_failed", "workspaceHandle() called before connect()");
@@ -69,6 +75,8 @@ export class RemoteContainerExecutionEnv {
69
75
  }
70
76
  if (this.destroyed)
71
77
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env already destroyed") };
78
+ if (this.homeRefusal)
79
+ return { ok: false, error: this.homeRefusal };
72
80
  if (this.paused)
73
81
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env is suspended (paused) — resumeVM(snapshotId) before use") };
74
82
  if (this.sbx)
@@ -95,6 +103,12 @@ export class RemoteContainerExecutionEnv {
95
103
  await sbx.kill().catch(() => { });
96
104
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
97
105
  }
106
+ const home = await this.homeVerdict(sbx, config?.abortSignal);
107
+ if (home !== undefined || this.destroyed) {
108
+ await sbx.kill().catch(() => { });
109
+ this.cfg.metrics?.inc("remote_env_connect_total", { provider: PROVIDER, result: "error" });
110
+ return { ok: false, error: home ?? new RemoteExecutionError("connect_failed", "execution env destroyed during connect") };
111
+ }
98
112
  this.sbx = sbx;
99
113
  this.paused = false;
100
114
  this.startKeepAlive();
@@ -147,6 +161,8 @@ export class RemoteContainerExecutionEnv {
147
161
  async resumeVM(snapshotId, options) {
148
162
  if (this.destroyed)
149
163
  return { ok: false, error: new RemoteExecutionError("connect_failed", "env already destroyed") };
164
+ if (this.homeRefusal)
165
+ return { ok: false, error: this.homeRefusal };
150
166
  if (this.sbx && !this.paused && this.sbx.sandboxId === snapshotId)
151
167
  return ok(this.workspaceHandle());
152
168
  if (this.connecting)
@@ -164,6 +180,16 @@ export class RemoteContainerExecutionEnv {
164
180
  await sbx.kill().catch(() => { });
165
181
  return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during resume") };
166
182
  }
183
+ const home = await this.homeVerdict(sbx, options?.abortSignal);
184
+ if (this.destroyed) {
185
+ await sbx.kill().catch(() => { });
186
+ return { ok: false, error: new RemoteExecutionError("connect_failed", "execution env destroyed during resume") };
187
+ }
188
+ if (home !== undefined) {
189
+ await this.rpc("sandbox.pause", (signal) => sbx.pause({ signal }), undefined, this.cfg.dataTimeoutMs).catch((e) => this.cfg.logger?.warn?.("remote_env_home_refused_repause_failed", { sandboxId: sbx.sandboxId, error: errMsg(e) }));
190
+ this.cfg.metrics?.inc("remote_env_resume_total", { provider: PROVIDER, result: "error" });
191
+ return { ok: false, error: home };
192
+ }
167
193
  this.sbx = sbx;
168
194
  this.paused = false;
169
195
  this.startKeepAlive();
@@ -196,6 +222,27 @@ export class RemoteContainerExecutionEnv {
196
222
  return { ok: false, error: this.remoteError(e, "post_resume_failed") };
197
223
  }
198
224
  }
225
+ async homeVerdict(sbx, signal) {
226
+ if (!this.homeSeat.asks)
227
+ return undefined;
228
+ let probe;
229
+ try {
230
+ const r = await this.rpc("sandbox.home", (s) => sbx.commands.run(LANE_HOME_PROBE_COMMAND, { signal: s }), signal);
231
+ probe = r.exitCode === 0 ? { ok: true, stdout: r.stdout } : { ok: false, reason: `exit ${r.exitCode}` };
232
+ }
233
+ catch (e) {
234
+ probe = { ok: false, reason: e instanceof CommandExitError ? `exit ${e.exitCode}` : errMsg(e) };
235
+ }
236
+ if (signal?.aborted)
237
+ return new RemoteExecutionError("aborted", "connect aborted while reading the sandbox's home");
238
+ const verdict = this.homeSeat.settle(probe);
239
+ if (verdict === undefined)
240
+ return undefined;
241
+ const error = new RemoteExecutionError("connect_failed", verdict.refusal);
242
+ if (verdict.answered)
243
+ this.homeRefusal = error;
244
+ return error;
245
+ }
199
246
  startKeepAlive() {
200
247
  this.stopKeepAlive();
201
248
  const everyMs = Math.max(30_000, Math.floor(this.cfg.timeoutMs * 0.8));
@@ -905,7 +952,7 @@ export class RemoteContainerExecutionEnv {
905
952
  async fsReady(p) {
906
953
  const conn = await this.ensureConnected();
907
954
  if (!conn.ok)
908
- return { ok: false, error: new FileError("unknown", `remote connect failed: ${conn.error.message}`, this.resolve(p), conn.error) };
955
+ return { ok: false, error: fileErrorFromExec(conn.error, this.resolve(p)) };
909
956
  return ok(conn.value);
910
957
  }
911
958
  tempResult(r, op) {
@@ -15,7 +15,7 @@
15
15
  * 那条教训:宣布"类修完成"而三个实现只落了一个)。收成一处之后,判据只有一条 ——
16
16
  * **文件面的 exec 包装必须走这里**,而这条能被机器检查(见 `test/env-adapter-file-error-parity.test.ts`)。
17
17
  */
18
- import { FileError, type ExecutionError, type RemoteExecutionError } from "@sema-agent/core";
18
+ import { FileError, RemoteExecutionError, type ExecutionError } from "@sema-agent/core";
19
19
  /** exec 层的错误码 → 文件层的码。只映射**有对应语义**的那些,其余落 `unknown`(诚实降级)。 */
20
20
  export declare function fileErrorFromExec(e: ExecutionError | RemoteExecutionError, path?: string): FileError;
21
21
  /** POSIX 的一个名字段上限(NAME_MAX,字节;Linux / macOS / 常见文件系统同值)。也是远端写的私有暂存目录名的上限(`remote-shell.ts` `buildStagingPaths`)。 */
@@ -1,6 +1,12 @@
1
- import { FileError } from "@sema-agent/core";
1
+ import { FileError, RemoteExecutionError } from "@sema-agent/core";
2
+ function neverOpened(e) {
3
+ if (e.code === "connect_failed")
4
+ return true;
5
+ const cause = e.cause;
6
+ return cause instanceof RemoteExecutionError && cause.code === "connect_failed";
7
+ }
2
8
  export function fileErrorFromExec(e, path) {
3
- const code = e.code === "aborted" ? "aborted" : "unknown";
9
+ const code = e.code === "aborted" ? "aborted" : neverOpened(e) ? "target_unavailable" : "unknown";
4
10
  return new FileError(code, e.message, path, e);
5
11
  }
6
12
  export const NAME_MAX_BYTES = 255;