@sema-agent/client-core 0.85.2 → 0.87.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 (79) hide show
  1. package/CHANGELOG.md +263 -0
  2. package/README.md +25 -16
  3. package/dist/adapt/arms.js +14 -1
  4. package/dist/adapter/downstream/terminalToSdkResult.d.ts +3 -3
  5. package/dist/adapter/downstream/terminalToSdkResult.js +4 -1
  6. package/dist/adapter/runStream.js +75 -2
  7. package/dist/adapter/types.d.ts +3 -0
  8. package/dist/agentSession/backgroundView.js +7 -1
  9. package/dist/agentSession/contract.d.ts +1 -1
  10. package/dist/centerWiringCapability.d.ts +38 -0
  11. package/dist/centerWiringCapability.js +202 -0
  12. package/dist/decideFailureNote.js +3 -0
  13. package/dist/decideReceipt.d.ts +1 -1
  14. package/dist/decideReceipt.js +1 -1
  15. package/dist/detachWire.d.ts +6 -1
  16. package/dist/detachWire.js +22 -1
  17. package/dist/engineCapReader.d.ts +8 -0
  18. package/dist/engineCapReader.js +15 -0
  19. package/dist/engineErrorCodes.d.ts +3 -0
  20. package/dist/engineErrorCodes.js +8 -0
  21. package/dist/engineNoticeCodes.d.ts +62 -0
  22. package/dist/engineNoticeCodes.js +247 -29
  23. package/dist/executionLaneCapability.d.ts +6 -0
  24. package/dist/executionLaneCapability.js +55 -2
  25. package/dist/fleet/fleetProjection.js +6 -8
  26. package/dist/fleet/workflowSizeWarning.js +2 -5
  27. package/dist/gateOutcome.d.ts +13 -0
  28. package/dist/gateOutcome.js +48 -1
  29. package/dist/gateVocabulary.d.ts +6 -3
  30. package/dist/gateVocabulary.js +7 -15
  31. package/dist/generated/engineFactTables.d.ts +10 -0
  32. package/dist/generated/engineFactTables.js +113 -0
  33. package/dist/generated/engineNoticeTables.js +14 -0
  34. package/dist/hitl/crashConverged.d.ts +1 -0
  35. package/dist/hitl/crashConverged.js +17 -2
  36. package/dist/hitl/hitlBridge.d.ts +4 -0
  37. package/dist/hitl/hitlBridge.js +6 -0
  38. package/dist/hitl/parkResolver.js +17 -12
  39. package/dist/hitl/refusedToolNameProse.d.ts +4 -0
  40. package/dist/hitl/refusedToolNameProse.js +7 -0
  41. package/dist/hitl/sessionPolicyDeliverable.js +9 -5
  42. package/dist/hitl/sessionPolicyWire.d.ts +16 -0
  43. package/dist/hitl/sessionPolicyWire.js +49 -1
  44. package/dist/hitl/suspendedReopen.d.ts +7 -0
  45. package/dist/hitl/suspendedReopen.js +38 -7
  46. package/dist/hitl/toolApprovalWire.js +4 -5
  47. package/dist/index.d.ts +6 -1
  48. package/dist/index.js +6 -1
  49. package/dist/inheritEnvWire.d.ts +22 -0
  50. package/dist/inheritEnvWire.js +84 -0
  51. package/dist/peerFrames.js +2 -10
  52. package/dist/printInitToolFace.d.ts +6 -0
  53. package/dist/printInitToolFace.js +115 -2
  54. package/dist/readCredentialRefusal.d.ts +2 -0
  55. package/dist/readCredentialRefusal.js +7 -0
  56. package/dist/request/taskRequest.d.ts +4 -5
  57. package/dist/request/taskRequest.js +33 -23
  58. package/dist/resumeRefusalCopy.d.ts +2 -7
  59. package/dist/resumeRefusalCopy.js +2 -24
  60. package/dist/rewindArchiveCapability.js +5 -0
  61. package/dist/runTerminal.js +2 -6
  62. package/dist/seam.d.ts +11 -1
  63. package/dist/seam.js +7 -0
  64. package/dist/sqlEngineCapability.js +15 -2
  65. package/dist/storePostureCapability.d.ts +25 -0
  66. package/dist/storePostureCapability.js +132 -0
  67. package/dist/toolHistoryMismatch.d.ts +63 -0
  68. package/dist/toolHistoryMismatch.js +425 -0
  69. package/dist/toolResult.js +2 -47
  70. package/dist/wireErrorTriage.d.ts +1 -0
  71. package/dist/wireErrorTriage.js +3 -0
  72. package/dist/wireFailureShape.d.ts +3 -0
  73. package/dist/wireFailureShape.js +44 -0
  74. package/dist/wireRefusalCopy.d.ts +39 -1
  75. package/dist/wireRefusalCopy.js +350 -2
  76. package/dist/workflowClient.d.ts +7 -2
  77. package/dist/workflowClient.js +15 -7
  78. package/docs/INTEGRATION-CLIENTS.md +942 -16
  79. package/package.json +2 -2
@@ -50,6 +50,39 @@ function readSettlement(v) {
50
50
  ...(note !== undefined ? { note } : {}),
51
51
  };
52
52
  }
53
+ function nonEmptyStringList(v) {
54
+ if (!Array.isArray(v) || v.length === 0)
55
+ return undefined;
56
+ const out = [];
57
+ for (const x of v) {
58
+ const t = str(x);
59
+ if (t === undefined)
60
+ return undefined;
61
+ out.push(t);
62
+ }
63
+ return Object.freeze(out);
64
+ }
65
+ function readModeRelease(raw) {
66
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
67
+ return undefined;
68
+ const r = raw;
69
+ const mode = str(r.mode);
70
+ const askClass = str(r.askClass);
71
+ const classes = nonEmptyStringList(r.classes);
72
+ if (mode === undefined || askClass === undefined || classes === undefined)
73
+ return undefined;
74
+ const origin = str(r.origin);
75
+ const mandate = str(r.mandate);
76
+ const reason = str(r.reason);
77
+ return {
78
+ mode,
79
+ askClass,
80
+ classes,
81
+ ...(origin !== undefined ? { origin } : {}),
82
+ ...(mandate !== undefined ? { mandate } : {}),
83
+ ...(reason !== undefined ? { reason } : {}),
84
+ };
85
+ }
53
86
  function readClassifierRound(raw) {
54
87
  if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
55
88
  return undefined;
@@ -85,18 +118,29 @@ export function gateOutcomeOf(frame) {
85
118
  let disposition;
86
119
  const classifier = readClassifierRound(dr.classifier);
87
120
  if (dk === 'allowed') {
88
- disposition = { kind: 'allowed', ...(classifier !== undefined ? { classifier } : {}) };
121
+ const released = readModeRelease(dr.released);
122
+ disposition = {
123
+ kind: 'allowed',
124
+ ...(classifier !== undefined ? { classifier } : {}),
125
+ ...(released !== undefined ? { released } : {}),
126
+ };
89
127
  }
90
128
  else if (dk === 'denied') {
91
129
  const deniedBy = str(dr.deniedBy);
92
130
  if (deniedBy === undefined)
93
131
  return undefined;
94
132
  const cause = str(dr.cause);
133
+ const mode = str(dr.mode);
134
+ const askClass = str(dr.askClass);
135
+ const classes = nonEmptyStringList(dr.classes);
95
136
  disposition = {
96
137
  kind: 'denied',
97
138
  deniedBy,
98
139
  ...(cause !== undefined ? { cause } : {}),
99
140
  ...(classifier !== undefined ? { classifier } : {}),
141
+ ...(mode !== undefined ? { mode } : {}),
142
+ ...(askClass !== undefined ? { askClass } : {}),
143
+ ...(classes !== undefined ? { classes } : {}),
100
144
  };
101
145
  }
102
146
  else {
@@ -115,6 +159,9 @@ export function isGateParkedToolEnd(frame) {
115
159
  return false;
116
160
  return frame.errorCode === GATE_PARKED_ERROR_CODE;
117
161
  }
162
+ export function gateModeRelease(g) {
163
+ return g?.disposition.kind === 'allowed' ? g.disposition.released : undefined;
164
+ }
118
165
  export function isDeniedGate(g) {
119
166
  return g?.disposition.kind === 'denied';
120
167
  }
@@ -1,12 +1,14 @@
1
- import type { DeniedBy } from '@sema-agent/sdk';
1
+ import { CORE_DENIED_BY_VALUES } from './generated/engineFactTables.js';
2
2
  import type { APPROVAL_MANDATE_WORDS } from './hitl/toolApprovalWire.js';
3
3
  export declare const CC_TOOL_DENIAL_KINDS: readonly ["user-rejected", "permission-rule", "automode-blocked", "automode-unavailable", "automode-parsing-error", "interrupted", "cancelled"];
4
4
  export type CcToolDenialKind = (typeof CC_TOOL_DENIAL_KINDS)[number];
5
5
  export declare function isCcToolDenialKind(v: unknown): v is CcToolDenialKind;
6
6
  export declare function isCcToolDenialKindADenial(v: unknown): boolean;
7
7
  export declare function ccToolDenialKindForSettledBy(settledBy: unknown): CcToolDenialKind | undefined;
8
- export declare const GATE_DENIED_BY_WORDS: readonly DeniedBy[];
9
- export declare function isGateDeniedByWord(v: unknown): v is DeniedBy;
8
+ declare const RETIRED_DENIED_BY_WORDS: readonly ["plan_mode"];
9
+ export type GateDeniedByWord = (typeof CORE_DENIED_BY_VALUES)[number] | (typeof RETIRED_DENIED_BY_WORDS)[number];
10
+ export declare const GATE_DENIED_BY_WORDS: readonly GateDeniedByWord[];
11
+ export declare function isGateDeniedByWord(v: unknown): v is GateDeniedByWord;
10
12
  export declare function gateDeniedByDetail(deniedBy: unknown): string;
11
13
  export declare function askOriginDetail(origin: unknown, message?: unknown): string;
12
14
  export declare const RULE_STORE_UNREADABLE_KINDS: readonly ["store", "call"];
@@ -17,3 +19,4 @@ export declare function mandatedApprovalDetail(): string;
17
19
  export type ApprovalMandateWord = (typeof APPROVAL_MANDATE_WORDS)[number];
18
20
  export declare function isApprovalMandateWord(v: unknown): v is ApprovalMandateWord;
19
21
  export declare function approvalMandateDetail(word: ApprovalMandateWord): string;
22
+ export {};
@@ -1,3 +1,4 @@
1
+ import { CORE_DENIED_BY_VALUES } from './generated/engineFactTables.js';
1
2
  export const CC_TOOL_DENIAL_KINDS = Object.freeze([
2
3
  'user-rejected',
3
4
  'permission-rule',
@@ -20,18 +21,10 @@ export function ccToolDenialKindForSettledBy(settledBy) {
20
21
  return 'permission-rule';
21
22
  return undefined;
22
23
  }
23
- export const GATE_DENIED_BY_WORDS = Object.freeze([
24
- 'policy',
25
- 'hook',
26
- 'org',
27
- 'persisted_rule',
28
- 'classifier',
29
- 'plan_mode',
30
- 'compliance',
31
- 'write_protection',
32
- 'ask_resolution',
33
- 'read_boundary',
34
- ]);
24
+ const RETIRED_DENIED_BY_WORDS = Object.freeze(['plan_mode']);
25
+ export const GATE_DENIED_BY_WORDS = Object.freeze([...CORE_DENIED_BY_VALUES, ...RETIRED_DENIED_BY_WORDS]);
26
+ const _deniedByWordsPin = true;
27
+ void _deniedByWordsPin;
35
28
  const DENIED_BY_SENTENCES = Object.freeze({
36
29
  policy: "denied by this deployment's permission policy",
37
30
  hook: 'denied by a PreToolUse hook',
@@ -43,6 +36,7 @@ const DENIED_BY_SENTENCES = Object.freeze({
43
36
  write_protection: 'denied because the write landed on a write-protected path',
44
37
  ask_resolution: 'denied when the approval was resolved',
45
38
  read_boundary: 'denied by the read boundary',
39
+ mode: "denied by this session's permission mode",
46
40
  });
47
41
  export function isGateDeniedByWord(v) {
48
42
  return typeof v === 'string' && GATE_DENIED_BY_WORDS.includes(v);
@@ -63,7 +57,7 @@ const ASK_ORIGIN_SENTENCES = Object.freeze({
63
57
  hook: 'a PreToolUse hook asked about this call',
64
58
  ask_rule: 'a persisted ask rule matches this call',
65
59
  denial_limit_fallback: 'the auto-mode classifier hit its denial limit and handed this call back to you',
66
- shell_gate_tighten: 'this deployment asks about shell commands the coarse shell gate rates above this tier',
60
+ shell_gate_tighten: 'the coarse shell check rates this command above the tier it lets through, so it asks',
67
61
  safety_tighten: 'the gate tightened on this call’s own facts (network egress, irreversibility, or a protected write)',
68
62
  policy: 'this deployment’s permission policy asks about this call',
69
63
  });
@@ -82,8 +76,6 @@ export function askOriginDetail(origin, message) {
82
76
  const word = typeof origin === 'string' && origin.length > 0 ? origin : '(none)';
83
77
  return `this call asks; its origin ${word} is a word newer than this client, so answer it as usual`;
84
78
  }
85
- const _deniedByWordsPin = GATE_DENIED_BY_WORDS;
86
- void _deniedByWordsPin;
87
79
  export const RULE_STORE_UNREADABLE_KINDS = Object.freeze(['store', 'call']);
88
80
  const RULE_STORE_UNREADABLE_SET = new Set(RULE_STORE_UNREADABLE_KINDS);
89
81
  export function isRuleStoreUnreadableKind(v) {
@@ -0,0 +1,10 @@
1
+ export declare const CORE_ASK_UNRESOLVABLE_CAUSES: readonly string[];
2
+ export declare const CORE_DURABLE_MANDATE_SOURCES: readonly string[];
3
+ export declare const CORE_TERMINAL_CAUSE_KINDS: readonly ["completed", "failed", "blocked", "paused"];
4
+ export declare const CORE_RESERVED_AGENT_NAMES: readonly string[];
5
+ export declare const CORE_WORKFLOW_SIZE_GUIDELINE_AGENT_CAPS: Readonly<Record<"small" | "medium" | "large", number>>;
6
+ export declare const CORE_ENGINE_AUTHORITY_ENVELOPE_TAGS: readonly string[];
7
+ export declare const CORE_DENIED_BY_VALUES: readonly ["policy", "hook", "org", "persisted_rule", "classifier", "compliance", "write_protection", "ask_resolution", "read_boundary", "mode"];
8
+ export declare const CORE_PERMISSION_MODE_FOLD_REASONS: readonly string[];
9
+ export declare const CORE_ENGINE_CARD_TYPES: readonly string[];
10
+ export declare const CORE_TOOL_DECLARATION_CODES: readonly string[];
@@ -0,0 +1,113 @@
1
+ export const CORE_ASK_UNRESOLVABLE_CAUSES = Object.freeze([
2
+ "approver_unavailable",
3
+ "no_approver_wired",
4
+ "mandate_unreconstructible",
5
+ ]);
6
+ export const CORE_DURABLE_MANDATE_SOURCES = Object.freeze([
7
+ "force_durable_gate",
8
+ "durable_approval",
9
+ ]);
10
+ export const CORE_TERMINAL_CAUSE_KINDS = Object.freeze([
11
+ "completed",
12
+ "failed",
13
+ "blocked",
14
+ "paused",
15
+ ]);
16
+ export const CORE_RESERVED_AGENT_NAMES = Object.freeze([
17
+ "StructuredOutput",
18
+ "ToolSearch",
19
+ "ReadToolResult",
20
+ "ReportBlocked",
21
+ "Agent",
22
+ ]);
23
+ export const CORE_WORKFLOW_SIZE_GUIDELINE_AGENT_CAPS = Object.freeze({
24
+ "small": 5,
25
+ "medium": 15,
26
+ "large": 50,
27
+ });
28
+ export const CORE_ENGINE_AUTHORITY_ENVELOPE_TAGS = Object.freeze([
29
+ "system-reminder",
30
+ "task-notification",
31
+ "new-diagnostics",
32
+ "user_memory",
33
+ "scope",
34
+ "skills",
35
+ "total_tokens",
36
+ "instruction-files",
37
+ ]);
38
+ export const CORE_DENIED_BY_VALUES = Object.freeze([
39
+ "policy",
40
+ "hook",
41
+ "org",
42
+ "persisted_rule",
43
+ "classifier",
44
+ "compliance",
45
+ "write_protection",
46
+ "ask_resolution",
47
+ "read_boundary",
48
+ "mode",
49
+ ]);
50
+ export const CORE_PERMISSION_MODE_FOLD_REASONS = Object.freeze([
51
+ "operator_deny",
52
+ "face_absent",
53
+ "inherited_ceiling",
54
+ ]);
55
+ export const CORE_ENGINE_CARD_TYPES = Object.freeze([
56
+ "mcp",
57
+ "a2a",
58
+ "path_not_in_root",
59
+ "read_path_denied",
60
+ "readonly_out_of_root",
61
+ "read_unestablished",
62
+ "text",
63
+ "notebook",
64
+ "file_unchanged",
65
+ "image",
66
+ "document",
67
+ "edit",
68
+ "create",
69
+ "update",
70
+ "notebook-edit",
71
+ "bash",
72
+ "bash_invalid_timeout",
73
+ "grep",
74
+ "glob",
75
+ "repo-map",
76
+ "artifact",
77
+ "worktree",
78
+ "task-output",
79
+ "task-stop",
80
+ "monitor-start",
81
+ "send-message",
82
+ "agent-transcript",
83
+ "list-agents",
84
+ "read-inbox",
85
+ "workflow-run",
86
+ "report-findings",
87
+ "tool-search",
88
+ "ask-question",
89
+ "exit-plan-mode",
90
+ "enter-plan-mode",
91
+ "cron-create",
92
+ "cron-delete",
93
+ "cron-list",
94
+ "schedule-wakeup",
95
+ "web-fetch",
96
+ "web-search",
97
+ "todo",
98
+ "task",
99
+ "task-list",
100
+ "agent",
101
+ "fork",
102
+ ]);
103
+ export const CORE_TOOL_DECLARATION_CODES = Object.freeze([
104
+ "config.tool_mount_denied",
105
+ "config.tool_roster_bound",
106
+ "config.tool_name_too_long",
107
+ "config.tool_contract_too_long",
108
+ "config.tool_schema_bound",
109
+ "config.tool_face_invalid",
110
+ "config.tool_name_invalid",
111
+ "config.egress_requires_write_effect",
112
+ "config.reserved_tool_name",
113
+ ]);
@@ -24,6 +24,11 @@ export const CORE_ENGINE_NOTICE_CODES = Object.freeze([
24
24
  "config.peer_admission_out_of_range",
25
25
  "config.peer_lane_unmounted",
26
26
  "config.secret_env_scrubbed",
27
+ "config.permission_mode_unavailable",
28
+ "permission.ask_released_by_mode",
29
+ "permission.read_deny_unapplied",
30
+ "permission.rule_store_unavailable",
31
+ "resume.legacy_mode_seed_migrated",
27
32
  "peer.inbound_disposition",
28
33
  "peer.held_settled",
29
34
  "peer.idle_subscription",
@@ -31,6 +36,7 @@ export const CORE_ENGINE_NOTICE_CODES = Object.freeze([
31
36
  "classifier.fallback",
32
37
  "classifier.role_defaulted",
33
38
  "classifier.contract_broken",
39
+ "classifier.unavailable",
34
40
  "checkpoint.execution_outcome_unrecorded",
35
41
  "delegation.transcript_integrity",
36
42
  "delegation.ask_unresolvable",
@@ -58,6 +64,7 @@ export const CORE_ENGINE_NOTICE_CODES = Object.freeze([
58
64
  "route.classifier_unroutable",
59
65
  "route.base_url_changed_key_unchanged",
60
66
  "route.response_model_mismatch",
67
+ "route.tool_call_id_folded",
61
68
  "task.user_steer_undrained",
62
69
  "task.user_followup_undrained",
63
70
  "steering.parked_input_blocked",
@@ -99,6 +106,11 @@ export const CORE_NOTICE_AUDIENCE = Object.freeze({
99
106
  "config.peer_admission_out_of_range": "operator",
100
107
  "config.peer_lane_unmounted": "operator",
101
108
  "config.secret_env_scrubbed": "operator",
109
+ "config.permission_mode_unavailable": "user",
110
+ "permission.ask_released_by_mode": "user",
111
+ "permission.read_deny_unapplied": "user",
112
+ "permission.rule_store_unavailable": "user",
113
+ "resume.legacy_mode_seed_migrated": "operator",
102
114
  "peer.inbound_disposition": "user",
103
115
  "peer.held_settled": "user",
104
116
  "peer.idle_subscription": "user",
@@ -106,6 +118,7 @@ export const CORE_NOTICE_AUDIENCE = Object.freeze({
106
118
  "classifier.fallback": "operator",
107
119
  "classifier.role_defaulted": "operator",
108
120
  "classifier.contract_broken": "operator",
121
+ "classifier.unavailable": "user",
109
122
  "checkpoint.execution_outcome_unrecorded": "operator",
110
123
  "delegation.transcript_integrity": "operator",
111
124
  "delegation.ask_unresolvable": "user",
@@ -133,6 +146,7 @@ export const CORE_NOTICE_AUDIENCE = Object.freeze({
133
146
  "route.classifier_unroutable": "operator",
134
147
  "route.base_url_changed_key_unchanged": "operator",
135
148
  "route.response_model_mismatch": "operator",
149
+ "route.tool_call_id_folded": "operator",
136
150
  "task.user_steer_undrained": "user",
137
151
  "task.user_followup_undrained": "user",
138
152
  "steering.parked_input_blocked": "user",
@@ -14,6 +14,7 @@ export interface CrashConvergedRow {
14
14
  readonly decidedAtMs?: number;
15
15
  readonly convergedAtMs: number;
16
16
  readonly resumeSafe: boolean;
17
+ readonly needsHumanReason?: 'approved_then_interrupted' | 'pending_unsafe' | 'unstable_row' | 'contradictory';
17
18
  }
18
19
  export interface ApprovalsListEnvelope {
19
20
  pending: PendingCheckpoint[];
@@ -71,6 +71,15 @@ function readCrashConvergedRow(v) {
71
71
  return null;
72
72
  }
73
73
  }
74
+ function needsHumanReasonOf(reading) {
75
+ if (reading.unstable)
76
+ return 'unstable_row';
77
+ if (reading.orphanState === 'pending' ? reading.hasApprovalEvidence : reading.resumeSafe === true)
78
+ return 'contradictory';
79
+ if (reading.orphanState === 'decided')
80
+ return 'approved_then_interrupted';
81
+ return 'pending_unsafe';
82
+ }
74
83
  export function projectCrashConverged(env) {
75
84
  const resumeSafe = [];
76
85
  const needsHuman = [];
@@ -96,10 +105,16 @@ export function projectCrashConverged(env) {
96
105
  }
97
106
  const row = reading.row;
98
107
  const consistentlyUnexecuted = reading.orphanState === 'pending' && !reading.hasApprovalEvidence;
99
- if (!reading.unstable && consistentlyUnexecuted && reading.resumeSafe === true)
108
+ if (!reading.unstable && consistentlyUnexecuted && reading.resumeSafe === true) {
109
+ Reflect.deleteProperty(row, 'needsHumanReason');
100
110
  resumeSafe.push(row);
101
- else
111
+ }
112
+ else {
113
+ Object.defineProperty(row, 'needsHumanReason', {
114
+ value: needsHumanReasonOf(reading), enumerable: true, writable: true, configurable: true,
115
+ });
102
116
  needsHuman.push(row);
117
+ }
103
118
  }
104
119
  }
105
120
  catch {
@@ -47,6 +47,10 @@ export type PlanReviewModeAfter = (typeof PLAN_REVIEW_MODE_AFTER_WORDS)[number];
47
47
  export declare function isPlanReviewModeAfter(v: string | undefined): v is PlanReviewModeAfter;
48
48
  export type HitlSafetyCode = 'binding_mismatch' | 'no_pending' | 'wrong_gate' | 'bad_plan_edit' | 'bad_plan_mode' | 'empty_answer';
49
49
  export type HitlFailureStage = 'fetch' | 'input' | 'card' | 'decide' | 'orchestration';
50
+ export declare const NO_APPROVAL_CARD_PORT_REASON = "no approval card port installed (print/non-REPL mode, or host forgot installApprovalCardPort)";
51
+ export declare const APPROVAL_CARD_PORT_KEY_MISMATCH_REASON = "no approval card port installed for the default session key, but a non-default sessionKey has one \u2014 likely a keyed-install/default-read mismatch (installApprovalCardPortFor vs the zero-arg default-key read), not a missing install";
52
+ export declare const NO_QUESTION_OVERLAY_REASON = "no question overlay mounted (print/non-REPL mode)";
53
+ export declare function isNoDecisionSurfaceReason(reason: string): boolean;
50
54
  export interface HitlDecideDenyOutcome {
51
55
  decision: 'deny';
52
56
  reason?: string | undefined;
@@ -25,6 +25,12 @@ export const PLAN_REVIEW_MODE_AFTER_WORDS = Object.freeze(['default', 'acceptEdi
25
25
  export function isPlanReviewModeAfter(v) {
26
26
  return v !== undefined && PLAN_REVIEW_MODE_AFTER_WORDS.includes(v);
27
27
  }
28
+ export const NO_APPROVAL_CARD_PORT_REASON = 'no approval card port installed (print/non-REPL mode, or host forgot installApprovalCardPort)';
29
+ export const APPROVAL_CARD_PORT_KEY_MISMATCH_REASON = 'no approval card port installed for the default session key, but a non-default sessionKey has one — likely a keyed-install/default-read mismatch (installApprovalCardPortFor vs the zero-arg default-key read), not a missing install';
30
+ export const NO_QUESTION_OVERLAY_REASON = 'no question overlay mounted (print/non-REPL mode)';
31
+ export function isNoDecisionSurfaceReason(reason) {
32
+ return reason === NO_APPROVAL_CARD_PORT_REASON || reason === APPROVAL_CARD_PORT_KEY_MISMATCH_REASON || reason === NO_QUESTION_OVERLAY_REASON;
33
+ }
28
34
  export class HitlSafetyError extends Error {
29
35
  code;
30
36
  constructor(message, code) {
@@ -1,5 +1,5 @@
1
1
  import { putOwnKey } from '../ownKey.js';
2
- import { DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, findPendingForTask, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
2
+ import { DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, NO_QUESTION_OVERLAY_REASON, findPendingForTask, isNoDecisionSurfaceReason, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
3
3
  import { PARKED_RESUME_STARTUP_FAILED, RESUME_POLICY_FOLD_CODE, isDecideParkMovedCode } from '../engineErrorCodes.js';
4
4
  import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
5
5
  import { hostLog } from '../host.js';
@@ -75,7 +75,7 @@ export function toAnsweredOutput(questions, answer) {
75
75
  }
76
76
  async function surfaceGateAndDecide(deps, taskId, askArgsByCall, signal, parkGatedCallId, onPresented) {
77
77
  if (!hasQuestionOverlay()) {
78
- return { kind: 'failed', stage: 'card', reason: 'no question overlay mounted (print/non-REPL mode)' };
78
+ return { kind: 'failed', stage: 'card', reason: NO_QUESTION_OVERLAY_REASON };
79
79
  }
80
80
  const found = await findPendingForTask(deps.client, taskId, isAskTool, signal ? { signal } : undefined, parkGatedCallId, (row) => isAskTool(typeof row.toolName === 'string' ? row.toolName : undefined));
81
81
  if (!found.ok)
@@ -301,7 +301,8 @@ export async function resolvePark(park, ctx) {
301
301
  : outcome.kind === 'retracted'
302
302
  ? 'the approval card was retracted by the host without a decision (nothing was sent; the run stays parked and remains decidable elsewhere)'
303
303
  : undefined;
304
- return { kind: 'failsoft', events: failsoftEvents(park, ctx, outcome.kind === 'aborted', failedReason) };
304
+ const noDecisionSurface = outcome.kind === 'failed' && outcome.stage === 'card' && isNoDecisionSurfaceReason(outcome.reason);
305
+ return { kind: 'failsoft', events: failsoftEvents(park, ctx, outcome.kind === 'aborted', failedReason, undefined, noDecisionSurface) };
305
306
  }
306
307
  led.dropHeldForDecidedPark(outcome.gatedCallId, park.gatedCallId);
307
308
  if (outcome.gatedCallId) {
@@ -324,7 +325,15 @@ export async function resolvePark(park, ctx) {
324
325
  ...(handoffTaskId !== undefined ? { handoffTaskId } : {}),
325
326
  };
326
327
  }
327
- function failsoftEvents(park, ctx, gateAbortedByUser, failedReason, terminal) {
328
+ function failsoftReasonSentence(park, failedReason, terminal) {
329
+ if (terminal !== undefined)
330
+ return terminal.errorMessage;
331
+ if (failedReason === undefined)
332
+ return undefined;
333
+ return (`${park.gate === 'fs' ? 'Tool approval' : 'AskUserQuestion'} gate could not be answered: ${failedReason}` +
334
+ ' — the run is still parked on this approval: decide it on the card when it is shown again, or cancel the run.');
335
+ }
336
+ function failsoftEvents(park, ctx, gateAbortedByUser, failedReason, terminal, noDecisionSurface = false) {
328
337
  const events = [
329
338
  ...flushHeldWithInterruptRewrite(ctx.led, {
330
339
  terminal: park.pendingDone,
@@ -332,19 +341,15 @@ function failsoftEvents(park, ctx, gateAbortedByUser, failedReason, terminal) {
332
341
  gateAbortedByUser,
333
342
  }),
334
343
  ];
344
+ const reason = failsoftReasonSentence(park, failedReason, terminal);
335
345
  if (park.pendingDone) {
336
- events.push(park.pendingDone);
346
+ events.push(reason === undefined || noDecisionSurface ? park.pendingDone : { ...park.pendingDone, _sema_park_failed_reason: reason });
337
347
  }
338
348
  else if (terminal !== undefined) {
339
349
  events.push({ type: 'failed', ...terminal });
340
350
  }
341
- else if (failedReason !== undefined) {
342
- events.push({
343
- type: 'failed',
344
- errorCode: 'hitl_unanswered',
345
- errorMessage: `${park.gate === 'fs' ? 'Tool approval' : 'AskUserQuestion'} gate could not be answered: ${failedReason}` +
346
- ' — the run is still parked on this approval: decide it on the card when it is shown again, or cancel the run.',
347
- });
351
+ else if (reason !== undefined) {
352
+ events.push({ type: 'failed', errorCode: 'hitl_unanswered', errorMessage: reason });
348
353
  }
349
354
  return events;
350
355
  }
@@ -0,0 +1,4 @@
1
+ export declare const PREFIXES_IN_PROSE: string;
2
+ export declare const REFUSED_NAME_CLASSES: string;
3
+ export declare const REFUSED_NAME_FIX: string;
4
+ export declare const RULES_LEGACY_TOOL_NAME_WRITE_NOTE: string;
@@ -0,0 +1,7 @@
1
+ import { CORE_PROTOCOL_PREFIXES } from '../generated/toolNameTables.js';
2
+ export const PREFIXES_IN_PROSE = CORE_PROTOCOL_PREFIXES.map((prefix) => `"${prefix}"`).join(' or ');
3
+ export const REFUSED_NAME_CLASSES = `a retired tool name, or a name that contains "__" but does not start with ${PREFIXES_IN_PROSE}`;
4
+ export const REFUSED_NAME_FIX = `If the tool still exists, use its current name; a server or agent-peer tool name starts with ${PREFIXES_IN_PROSE}.`;
5
+ export const RULES_LEGACY_TOOL_NAME_WRITE_NOTE = `The engine refused to write these session rules, and the stored record was not changed at all: they name a tool the engine refuses — ${REFUSED_NAME_CLASSES} (the engine's own error text names the entry). ` +
6
+ `${REFUSED_NAME_FIX} Then write the rules again. ` +
7
+ 'If that entry was already in this session’s rule record, it has to be removed from the record before the record can be written again.';
@@ -1,16 +1,16 @@
1
- import { CONFIG_LEGACY_TOOL_NAME } from '../engineErrorCodes.js';
1
+ import { CONFIG_LEGACY_TOOL_NAME, RULES_LEGACY_TOOL_NAME } from '../engineErrorCodes.js';
2
2
  import { rosterMountedNames } from './rosterMountedNames.js';
3
+ import { PREFIXES_IN_PROSE, REFUSED_NAME_CLASSES, REFUSED_NAME_FIX, RULES_LEGACY_TOOL_NAME_WRITE_NOTE } from './refusedToolNameProse.js';
3
4
  import { CORE_PROTOCOL_PREFIXES, CORE_RETIRED_TOOL_CURRENT_NAMES } from '../generated/toolNameTables.js';
4
5
  const COVERING_NAMESPACE_PREFIXES = CORE_PROTOCOL_PREFIXES;
5
6
  const ENGINE_RETIRED_TOOL_NAMES = CORE_RETIRED_TOOL_CURRENT_NAMES;
6
- const PREFIXES_IN_PROSE = COVERING_NAMESPACE_PREFIXES.map((prefix) => `"${prefix}"`).join(' or ');
7
7
  const WITHHELD_NOTICE = {
8
8
  ask_no_bucket: 'Not written to this session’s rule record: that record only holds rules that block outright, and this rule asks before running instead — recording it as a block would change what it means.',
9
9
  qualified_deny: 'Not written to this session’s rule record: that record holds whole tool names and command names only, and this rule has a part in parentheses it cannot carry — recording just the name could block more than this rule does.',
10
10
  peer_wide: 'Not written to this session’s rule record: this rule names a server or agent peer without naming one of its tools, and an engine that matches that record by exact tool name would block none of that peer’s tools.',
11
11
  wildcard: 'Not written to this session’s rule record: this rule uses a wildcard (*), and an engine that matches that record by exact tool name would block none of the tools it covers.',
12
12
  not_a_tool_name: 'Not written to this session’s rule record: this entry is not exactly one tool name — it is empty, not text, or has spaces around the name.',
13
- legacy_tool_name: `Not written to this session’s rule record: unless a tool by exactly this name is available, the engine refuses it — it is a retired tool name, or it contains "__" but does not start with ${PREFIXES_IN_PROSE} — and with it in that record every later run of this session would fail at startup until that entry is removed from the record. If the tool still exists, use its current name; a server or agent-peer tool name starts with ${PREFIXES_IN_PROSE}.`,
13
+ legacy_tool_name: `Not written to this session’s rule record: unless a tool by exactly this name is available, the engine refuses it — it is a retired tool name, or it contains "__" but does not start with ${PREFIXES_IN_PROSE} — and with it in that record every later run of this session would fail at startup until that entry is removed from the record. ${REFUSED_NAME_FIX}`,
14
14
  not_in_roster: 'Not written to this session’s rule record: the tool roster supplied for this check has no tool by exactly this name, as a name or an alias, with letter case counted — it may be misspelled, or name a tool this deployment does not provide — so recorded as it is it would block nothing here.',
15
15
  };
16
16
  export const SESSION_POLICY_WITHHELD_WHY = Object.freeze(Object.keys(WITHHELD_NOTICE));
@@ -114,8 +114,12 @@ export function sessionPolicyWithheldNotice(why) {
114
114
  ? WITHHELD_NOTICE[why]
115
115
  : 'Not written to this session’s rule record.';
116
116
  }
117
- const LEGACY_TOOL_NAME_FAILURE_NOTE = `This run did not start: a tool rule applied to it names a tool the engine refuses — a retired tool name, or a name that contains "__" but does not start with ${PREFIXES_IN_PROSE} (the engine's own error text names the entry). ` +
117
+ const LEGACY_TOOL_NAME_FAILURE_NOTE = `This run did not start: a tool rule applied to it names a tool the engine refuses — ${REFUSED_NAME_CLASSES} (the engine's own error text names the entry). ` +
118
118
  'Every later run fails the same way until that entry is removed from wherever it is set: this session’s rule record, the settings sent with the run, a parent session’s rule record, or the deployment’s own tool rules.';
119
119
  export function legacyToolNameFailureNoteOf(code) {
120
- return code === CONFIG_LEGACY_TOOL_NAME ? LEGACY_TOOL_NAME_FAILURE_NOTE : undefined;
120
+ if (code === CONFIG_LEGACY_TOOL_NAME)
121
+ return LEGACY_TOOL_NAME_FAILURE_NOTE;
122
+ if (code === RULES_LEGACY_TOOL_NAME)
123
+ return RULES_LEGACY_TOOL_NAME_WRITE_NOTE;
124
+ return undefined;
121
125
  }
@@ -38,6 +38,11 @@ export type SessionPolicyFailure = {
38
38
  } | {
39
39
  kind: 'request-rejected';
40
40
  message: string;
41
+ } | {
42
+ kind: 'legacy-tool-name';
43
+ message: string;
44
+ field?: SessionPolicyRemovedEntry['field'];
45
+ name?: string;
41
46
  } | {
42
47
  kind: 'unauthorized';
43
48
  message: string;
@@ -85,6 +90,12 @@ export type SessionPolicyTightenOutcome = {
85
90
  } | {
86
91
  kind: 'capability_absent';
87
92
  why: 'not_reported' | 'absent';
93
+ } | {
94
+ kind: 'legacy_tool_name';
95
+ message: string;
96
+ field?: SessionPolicyRemovedEntry['field'];
97
+ name?: string;
98
+ alreadyInRecord?: boolean;
88
99
  } | {
89
100
  kind: 'unknown';
90
101
  why: SessionPolicyTightenUnknownWhy;
@@ -127,6 +138,11 @@ export type SessionPolicyRemovalOutcome = {
127
138
  kind: 'write_unconfirmed';
128
139
  why: Extract<SessionPolicyTightenUnknownWhy, 'write_indeterminate' | 'write_receipt_unreadable' | 'write_receipt_mismatch'>;
129
140
  message?: string;
141
+ } | {
142
+ kind: 'legacy_tool_name';
143
+ message: string;
144
+ field?: SessionPolicyRemovedEntry['field'];
145
+ name?: string;
130
146
  };
131
147
  export type SessionPolicyRosterRequiredWhy = 'absent' | 'unreadable';
132
148
  export declare function removeRefusedSessionRules(facade: SessionPolicyFacade, sessionId: string, roster: ToolRosterView | undefined, opts?: {