@sema-agent/client-core 0.83.0 → 0.83.2

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.
@@ -0,0 +1,52 @@
1
+ import { type EngineAgentPanelAbsence, type EngineAgentPanelCycleIdentity, type EngineAgentPanelEvent } from './engineAgentPanelStore.js';
2
+ import type { RunTerminalStatus } from './runTerminal.js';
3
+ export declare const ENGINE_AGENT_ABSENT_ROW_TEXT = "engine no longer reports this agent \u00B7 it may still be running";
4
+ export declare const ENGINE_AGENT_ABSENCE_TTL_MS: number;
5
+ export type EngineAgentAbsenceMark = Readonly<Pick<EngineAgentPanelAbsence, 'lastSeenAtMs' | 'absentForMs'>>;
6
+ export interface EngineAgentAbsenceRowFacts {
7
+ readonly status: string;
8
+ readonly absence?: EngineAgentAbsenceMark;
9
+ readonly viewing?: boolean;
10
+ }
11
+ type AbsenceOnly = Pick<EngineAgentAbsenceRowFacts, 'absence'> | null | undefined;
12
+ type StatusAndAbsence = Pick<EngineAgentAbsenceRowFacts, 'status' | 'absence'>;
13
+ export declare function isEngineAgentAbsentRow(row: AbsenceOnly): boolean;
14
+ export declare function engineAgentAbsenceMarkOf(row: StatusAndAbsence, ev: Pick<EngineAgentPanelAbsence, 'lastSeenAtMs' | 'absentForMs'>): EngineAgentAbsenceMark | null;
15
+ export declare function engineAgentPanelEventClearsAbsence(ev: EngineAgentPanelEvent, current?: EngineAgentPanelCycleIdentity | null): boolean;
16
+ export declare function engineAgentAbsenceClockStopMs(row: AbsenceOnly): number | undefined;
17
+ export declare function engineAgentRowCountsAsRunning(row: StatusAndAbsence): boolean;
18
+ export interface EngineAgentRowTally {
19
+ readonly running: number;
20
+ readonly absent: number;
21
+ readonly byStatus: Readonly<Record<string, number>>;
22
+ }
23
+ export declare function tallyEngineAgentRows(rows: readonly StatusAndAbsence[] | Readonly<Record<string, StatusAndAbsence>>): EngineAgentRowTally;
24
+ export declare function engineAgentTurnSweepSettles(taskId: string, row: StatusAndAbsence): boolean;
25
+ export declare function engineAgentAbsenceTtlDueAtMs(row: AbsenceOnly, ttlMs?: number): number | undefined;
26
+ export declare const ENGINE_AGENT_REGISTRY_READINGS: readonly ["running", "ended", "not_listed", "unknown"];
27
+ export type EngineAgentRegistryReading = (typeof ENGINE_AGENT_REGISTRY_READINGS)[number];
28
+ export interface EngineAgentAbsenceReapRow {
29
+ readonly absence?: EngineAgentAbsenceMark;
30
+ readonly viewing?: boolean;
31
+ readonly registry?: EngineAgentRegistryReading;
32
+ }
33
+ export interface EngineAgentAbsenceReclaim {
34
+ readonly taskId: string;
35
+ readonly lastSeenAtMs: number;
36
+ readonly absentForMs: number;
37
+ readonly dueAtMs: number;
38
+ readonly registry: 'ended' | 'not_listed';
39
+ }
40
+ export interface EngineAgentAbsenceReap {
41
+ readonly reclaim: readonly EngineAgentAbsenceReclaim[];
42
+ readonly heldByViewer: readonly string[];
43
+ }
44
+ export interface EngineAgentAbsenceReapOptions {
45
+ readonly nowMs: number;
46
+ readonly ttlMs?: number;
47
+ readonly owned?: ReadonlySet<string>;
48
+ }
49
+ export declare function reapEngineAgentAbsentRows(rows: Readonly<Record<string, EngineAgentAbsenceReapRow | null | undefined>>, opts: EngineAgentAbsenceReapOptions): EngineAgentAbsenceReap;
50
+ export declare function engineAgentAbsenceDroppedLine(label: string, ttlMs?: number, removeReason?: string | null): string;
51
+ export declare function engineAgentTerminalAfterDropLine(label: string, status: RunTerminalStatus, hasReport: boolean): string;
52
+ export {};
@@ -0,0 +1,131 @@
1
+ import { displayUntrusted } from './displayUntrusted.js';
2
+ import { isEnginePanelTaskResident, isStaleEngineAgentPanelEnd, } from './engineAgentPanelStore.js';
3
+ export const ENGINE_AGENT_ABSENT_ROW_TEXT = 'engine no longer reports this agent · it may still be running';
4
+ export const ENGINE_AGENT_ABSENCE_TTL_MS = 30 * 60_000;
5
+ function effectiveTtl(ttlMs) {
6
+ return typeof ttlMs === 'number' && Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : ENGINE_AGENT_ABSENCE_TTL_MS;
7
+ }
8
+ function markOf(row) {
9
+ if (typeof row !== 'object' || row === null)
10
+ return undefined;
11
+ const a = row.absence;
12
+ return typeof a === 'object' && a !== null ? a : undefined;
13
+ }
14
+ export function isEngineAgentAbsentRow(row) {
15
+ return markOf(row) !== undefined;
16
+ }
17
+ export function engineAgentAbsenceMarkOf(row, ev) {
18
+ if (row?.status !== 'running')
19
+ return null;
20
+ if (isEngineAgentAbsentRow(row))
21
+ return null;
22
+ if (typeof ev?.lastSeenAtMs !== 'number' || !Number.isFinite(ev.lastSeenAtMs))
23
+ return null;
24
+ return { lastSeenAtMs: ev.lastSeenAtMs, absentForMs: ev.absentForMs };
25
+ }
26
+ export function engineAgentPanelEventClearsAbsence(ev, current) {
27
+ if (typeof ev !== 'object' || ev === null)
28
+ return false;
29
+ switch (ev.kind) {
30
+ case 'tick':
31
+ case 'fleet-row':
32
+ return true;
33
+ case 'end':
34
+ return !isStaleEngineAgentPanelEnd(ev, current);
35
+ default:
36
+ return false;
37
+ }
38
+ }
39
+ export function engineAgentAbsenceClockStopMs(row) {
40
+ return markOf(row)?.lastSeenAtMs;
41
+ }
42
+ export function engineAgentRowCountsAsRunning(row) {
43
+ if (typeof row !== 'object' || row === null)
44
+ return false;
45
+ if (isEngineAgentAbsentRow(row))
46
+ return false;
47
+ return row.status === 'running' || row.status === 'pending';
48
+ }
49
+ export function tallyEngineAgentRows(rows) {
50
+ const list = Array.isArray(rows) ? rows : Object.values(rows ?? {});
51
+ let running = 0;
52
+ let absent = 0;
53
+ const byStatus = Object.create(null);
54
+ for (const row of list) {
55
+ if (typeof row !== 'object' || row === null)
56
+ continue;
57
+ const r = row;
58
+ if (isEngineAgentAbsentRow(r)) {
59
+ absent++;
60
+ continue;
61
+ }
62
+ if (engineAgentRowCountsAsRunning(r)) {
63
+ running++;
64
+ continue;
65
+ }
66
+ const word = String(r.status);
67
+ byStatus[word] = (byStatus[word] ?? 0) + 1;
68
+ }
69
+ return { running, absent, byStatus };
70
+ }
71
+ export function engineAgentTurnSweepSettles(taskId, row) {
72
+ if (typeof row !== 'object' || row === null)
73
+ return false;
74
+ if (isEnginePanelTaskResident(taskId))
75
+ return false;
76
+ if (row.status !== 'running')
77
+ return false;
78
+ return !isEngineAgentAbsentRow(row);
79
+ }
80
+ export function engineAgentAbsenceTtlDueAtMs(row, ttlMs) {
81
+ const a = markOf(row);
82
+ if (a === undefined || typeof a.lastSeenAtMs !== 'number' || !Number.isFinite(a.lastSeenAtMs))
83
+ return undefined;
84
+ return a.lastSeenAtMs + effectiveTtl(ttlMs);
85
+ }
86
+ export const ENGINE_AGENT_REGISTRY_READINGS = Object.freeze(['running', 'ended', 'not_listed', 'unknown']);
87
+ function registryConfirmsGone(reading) {
88
+ return reading === 'ended' || reading === 'not_listed';
89
+ }
90
+ export function reapEngineAgentAbsentRows(rows, opts) {
91
+ const reclaim = [];
92
+ const heldByViewer = [];
93
+ const now = opts?.nowMs;
94
+ if (typeof now !== 'number' || !Number.isFinite(now))
95
+ return { reclaim, heldByViewer };
96
+ const ttl = effectiveTtl(opts.ttlMs);
97
+ for (const [taskId, row] of Object.entries(rows ?? {})) {
98
+ if (opts.owned !== undefined && !opts.owned.has(taskId))
99
+ continue;
100
+ const a = markOf(row);
101
+ if (a === undefined || typeof a.lastSeenAtMs !== 'number' || !Number.isFinite(a.lastSeenAtMs))
102
+ continue;
103
+ const dueAtMs = a.lastSeenAtMs + ttl;
104
+ if (now < dueAtMs)
105
+ continue;
106
+ const registry = row?.registry;
107
+ if (!registryConfirmsGone(registry))
108
+ continue;
109
+ if (row?.viewing === true) {
110
+ heldByViewer.push(taskId);
111
+ continue;
112
+ }
113
+ reclaim.push({ taskId, lastSeenAtMs: a.lastSeenAtMs, absentForMs: a.absentForMs, dueAtMs, registry });
114
+ }
115
+ return { reclaim, heldByViewer };
116
+ }
117
+ const REMOVE_REASON_DISPLAY_MAX = 64;
118
+ export function engineAgentAbsenceDroppedLine(label, ttlMs, removeReason) {
119
+ const minutes = Math.round(effectiveTtl(ttlMs) / 60_000);
120
+ const reason = typeof removeReason === 'string' && removeReason !== ''
121
+ ? displayUntrusted(removeReason, { max: REMOVE_REASON_DISPLAY_MAX })
122
+ : '';
123
+ const why = reason !== ''
124
+ ? ` · the engine removed it from the fleet list (${reason}); that is not an outcome — read the run to settle it`
125
+ : ' · outcome unknown';
126
+ return `${displayUntrusted(label)} was dropped from the task panel after ${minutes} min without an engine report${why}`;
127
+ }
128
+ export function engineAgentTerminalAfterDropLine(label, status, hasReport) {
129
+ const tail = hasReport ? ' · its final report arrived too late to keep in the panel' : '';
130
+ return `${displayUntrusted(label)} reported ${displayUntrusted(status)} after it was dropped from the task panel${tail}`;
131
+ }
@@ -65,6 +65,8 @@ export const ENGINE_NOTICE_CODES = Object.freeze([
65
65
  'task.halt_unconsumed',
66
66
  'task.late_approval',
67
67
  'task.interrupt_unconsumed',
68
+ 'approval.read_root_granted',
69
+ 'approval.read_root_grant_rejected',
68
70
  'memory.capture_opted_out',
69
71
  'memory.capture_optout_unpersisted',
70
72
  'skills.listing_truncated',
@@ -142,6 +144,8 @@ export const ENGINE_NOTICE_AUDIENCE = Object.freeze({
142
144
  'task.halt_unconsumed': 'user',
143
145
  'task.late_approval': 'user',
144
146
  'task.interrupt_unconsumed': 'user',
147
+ 'approval.read_root_granted': 'user',
148
+ 'approval.read_root_grant_rejected': 'user',
145
149
  'memory.capture_opted_out': 'user',
146
150
  'memory.capture_optout_unpersisted': 'user',
147
151
  'skills.listing_truncated': 'operator',
@@ -1,6 +1,6 @@
1
- const DISPLAY_UNSAFE = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]|[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g;
1
+ import { applyDisplayFace, LEGACY_ESCAPE_FACE, LEGACY_LABEL_FACE } from './displayUntrusted.js';
2
2
  export function escapeDisplayControlChars(s) {
3
- return s.replace(DISPLAY_UNSAFE, ch => `\\u${(ch.codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, '0')}`);
3
+ return applyDisplayFace(s, LEGACY_ESCAPE_FACE);
4
4
  }
5
5
  export function capForDisplay(raw, max) {
6
6
  const escaped = escapeDisplayControlChars(raw);
@@ -19,7 +19,7 @@ export function capForDisplay(raw, max) {
19
19
  return cut > 0 ? escaped.slice(0, cut) : '';
20
20
  }
21
21
  export function collapseLabel(s) {
22
- return escapeDisplayControlChars(s.replace(/\s+/g, ' ').trim());
22
+ return applyDisplayFace(s, LEGACY_LABEL_FACE);
23
23
  }
24
24
  export function taskDescFromName(name, agentType, agentName) {
25
25
  if (!name || !agentType)
@@ -14,3 +14,7 @@ export type RuleStoreUnreadableKind = (typeof RULE_STORE_UNREADABLE_KINDS)[numbe
14
14
  export declare function isRuleStoreUnreadableKind(v: unknown): v is RuleStoreUnreadableKind;
15
15
  export declare function ruleStoreUnreadableDetail(kind: RuleStoreUnreadableKind): string;
16
16
  export declare function mandatedApprovalDetail(): string;
17
+ export declare const APPROVAL_MANDATE_WORDS: readonly ["operator_always", "tool_marks", "probe_mandate", "probe_unanswered", "write_protection", "write_protection_unresolved"];
18
+ export type ApprovalMandateWord = (typeof APPROVAL_MANDATE_WORDS)[number];
19
+ export declare function isApprovalMandateWord(v: unknown): v is ApprovalMandateWord;
20
+ export declare function approvalMandateDetail(word: ApprovalMandateWord): string;
@@ -116,5 +116,33 @@ export function ruleStoreUnreadableDetail(kind) {
116
116
  return `your standing deny/ask rules could not be applied to this call; the reported reason ${word} is not one of the mechanisms this client's closed set knows, so answer this ask as usual`;
117
117
  }
118
118
  export function mandatedApprovalDetail() {
119
- return 'no saved allow rule and no standing posture can retire this question, so it is asked every time — answering it here settles this one call';
119
+ return 'no saved allow rule and no remembered answer can retire this question; each call is settled on its own — by you here, by a hook, or by an automatic check the deployment runs — and an answer covers only that call';
120
+ }
121
+ export const APPROVAL_MANDATE_WORDS = Object.freeze([
122
+ 'operator_always',
123
+ 'tool_marks',
124
+ 'probe_mandate',
125
+ 'probe_unanswered',
126
+ 'write_protection',
127
+ 'write_protection_unresolved',
128
+ ]);
129
+ const _mandateWordsPin = true;
130
+ void _mandateWordsPin;
131
+ const APPROVAL_MANDATE_SET = new Set(APPROVAL_MANDATE_WORDS);
132
+ export function isApprovalMandateWord(v) {
133
+ return APPROVAL_MANDATE_SET.has(v);
134
+ }
135
+ const APPROVAL_MANDATE_SENTENCES = Object.freeze({
136
+ operator_always: 'this deployment requires a separate confirmation for each shell command, and this call is one of them',
137
+ tool_marks: 'this tool declares that it reaches outside this machine or makes changes that cannot be undone',
138
+ probe_mandate: 'the check run on this call marked it as needing its own confirmation (the built-in shell check does so for a read outside the directories allowed for this session)',
139
+ probe_unanswered: 'the check run on this call did not answer (it timed out or failed), so whether the call stays inside the directories allowed for this session is unknown',
140
+ write_protection: "this change lands on a protected location (the agent's own configuration), which is confirmed one change at a time",
141
+ write_protection_unresolved: 'the protected-location check could not tell where this change lands, so whether it touches a protected location is unknown',
142
+ });
143
+ export function approvalMandateDetail(word) {
144
+ if (typeof word === 'string' && Object.hasOwn(APPROVAL_MANDATE_SENTENCES, word)) {
145
+ return APPROVAL_MANDATE_SENTENCES[word];
146
+ }
147
+ return 'the reason reported for this confirmation is not one this client recognises, so it shows no reason';
120
148
  }
@@ -1,10 +1,12 @@
1
+ import { displayUntrusted } from '../displayUntrusted.js';
1
2
  const NOTE_DISPLAY_MAX = 200;
2
3
  export const DECISION_NOTE_NOTICE_KEY = 'approval-decision-note';
3
4
  function cleanNote(raw) {
4
- const flat = raw
5
- .replace(/[\u0000-\u001f\u007f-\u009f]+/g, ' ')
5
+ const flat = displayUntrusted(raw, { credentialUrls: false, credentialWords: false, foldLines: false, mark: 'space' })
6
6
  .replace(/ {2,}/g, ' ')
7
7
  .trim();
8
+ if (flat === '')
9
+ return undefined;
8
10
  return flat.length <= NOTE_DISPLAY_MAX ? flat : `${flat.slice(0, NOTE_DISPLAY_MAX - 1)}…`;
9
11
  }
10
12
  export function readDecisionNoteAudit(ack) {
@@ -0,0 +1,15 @@
1
+ export interface AskPostureFacts {
2
+ readonly settingsAskRuleMatched?: boolean;
3
+ }
4
+ export type AskPostureSurvivalRoad = 'origin' | 'settings_ask_rule';
5
+ export type AskPostureVerdict = {
6
+ readonly kind: 'must_ask';
7
+ readonly by: AskPostureSurvivalRoad;
8
+ } | {
9
+ readonly kind: 'posture_may_answer';
10
+ } | {
11
+ readonly kind: 'unknown';
12
+ };
13
+ export declare function askSurvivesPosture(req: {
14
+ readonly origin?: string;
15
+ } | null | undefined, facts?: AskPostureFacts | null): AskPostureVerdict;
@@ -0,0 +1,31 @@
1
+ import { readOwnNonEmptyString } from '../ownKey.js';
2
+ const ORIGIN_SURVIVES_POSTURE = Object.freeze({
3
+ content_question: true,
4
+ ancestor_marked: false,
5
+ org_unavailable: true,
6
+ org_rule: true,
7
+ rule_store_unavailable: true,
8
+ hook: true,
9
+ ask_rule: true,
10
+ denial_limit_fallback: true,
11
+ shell_gate_tighten: false,
12
+ safety_tighten: false,
13
+ policy: false,
14
+ });
15
+ const MUST_ASK_BY_ORIGIN = Object.freeze({ kind: 'must_ask', by: 'origin' });
16
+ const MUST_ASK_BY_SETTINGS = Object.freeze({ kind: 'must_ask', by: 'settings_ask_rule' });
17
+ const POSTURE_MAY_ANSWER = Object.freeze({ kind: 'posture_may_answer' });
18
+ const UNKNOWN = Object.freeze({ kind: 'unknown' });
19
+ export function askSurvivesPosture(req, facts) {
20
+ const origin = readOwnNonEmptyString(req, 'origin');
21
+ const rawMatched = typeof facts === 'object' && facts !== null ? facts.settingsAskRuleMatched : undefined;
22
+ const matched = typeof rawMatched === 'boolean' ? rawMatched : undefined;
23
+ const known = origin !== undefined && Object.hasOwn(ORIGIN_SURVIVES_POSTURE, origin);
24
+ if (known && ORIGIN_SURVIVES_POSTURE[origin] === true)
25
+ return MUST_ASK_BY_ORIGIN;
26
+ if (matched === true)
27
+ return MUST_ASK_BY_SETTINGS;
28
+ if (matched === false && (origin === undefined || known))
29
+ return POSTURE_MAY_ANSWER;
30
+ return UNKNOWN;
31
+ }
@@ -0,0 +1,14 @@
1
+ import type { SessionPermissionRules } from '@sema-agent/sdk';
2
+ export type SessionPolicyWithheldWhy = 'ask_no_bucket' | 'qualified_deny' | 'peer_wide' | 'wildcard' | 'not_a_tool_name';
3
+ export declare const SESSION_POLICY_WITHHELD_WHY: readonly SessionPolicyWithheldWhy[];
4
+ export interface SessionPolicyWithheldRule {
5
+ readonly rule: string;
6
+ readonly why: SessionPolicyWithheldWhy;
7
+ }
8
+ export interface SessionPolicyDeliverability {
9
+ readonly deliverable: SessionPermissionRules;
10
+ readonly withheld: readonly SessionPolicyWithheldRule[];
11
+ readonly unreadable?: true;
12
+ }
13
+ export declare function sessionPolicyDeliverable(behavior: 'deny' | 'ask', rules: readonly string[]): SessionPolicyDeliverability;
14
+ export declare function sessionPolicyWithheldNotice(why: SessionPolicyWithheldWhy): string;
@@ -0,0 +1,80 @@
1
+ const WITHHELD_NOTICE = {
2
+ 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.',
3
+ 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.',
4
+ peer_wide: 'Not written to this session’s rule record: this rule covers every tool of one server or agent peer, and an engine that matches that record by exact tool name would block none of them.',
5
+ 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.',
6
+ 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.',
7
+ };
8
+ export const SESSION_POLICY_WITHHELD_WHY = Object.freeze(Object.keys(WITHHELD_NOTICE));
9
+ const MAX_RULES = 10_000;
10
+ function unreadable() {
11
+ return { deliverable: {}, withheld: [], unreadable: true };
12
+ }
13
+ const COVERING_NAMESPACE_PREFIXES = ['mcp__', 'a2a__'];
14
+ function withheldWhyOf(rule) {
15
+ if (rule.length === 0)
16
+ return 'not_a_tool_name';
17
+ if (/(?:^|[^\\])\(/.test(rule))
18
+ return 'qualified_deny';
19
+ if (rule.includes('*'))
20
+ return 'wildcard';
21
+ for (const prefix of COVERING_NAMESPACE_PREFIXES) {
22
+ if (!rule.startsWith(prefix))
23
+ continue;
24
+ const segments = rule.slice(prefix.length).split('__');
25
+ if (segments[0] !== '' && segments.slice(1).join('__') === '')
26
+ return 'peer_wide';
27
+ }
28
+ if (rule.trim() !== rule)
29
+ return 'not_a_tool_name';
30
+ return undefined;
31
+ }
32
+ export function sessionPolicyDeliverable(behavior, rules) {
33
+ const entries = [];
34
+ try {
35
+ if (behavior !== 'deny' && behavior !== 'ask')
36
+ return unreadable();
37
+ if (!Array.isArray(rules))
38
+ return unreadable();
39
+ const n = rules.length;
40
+ if (typeof n !== 'number' || !Number.isSafeInteger(n) || n < 0 || n > MAX_RULES)
41
+ return unreadable();
42
+ for (let i = 0; i < n; i++) {
43
+ if (!Object.hasOwn(rules, i)) {
44
+ entries.push(undefined);
45
+ continue;
46
+ }
47
+ const x = rules[i];
48
+ entries.push(typeof x === 'string' ? x : undefined);
49
+ }
50
+ }
51
+ catch {
52
+ return unreadable();
53
+ }
54
+ const withheld = [];
55
+ if (behavior === 'ask') {
56
+ for (const r of entries)
57
+ withheld.push({ rule: r ?? '', why: 'ask_no_bucket' });
58
+ return { deliverable: {}, withheld };
59
+ }
60
+ const exact = [];
61
+ for (const r of entries) {
62
+ if (r === undefined) {
63
+ withheld.push({ rule: '', why: 'not_a_tool_name' });
64
+ continue;
65
+ }
66
+ const why = withheldWhyOf(r);
67
+ if (why === undefined)
68
+ exact.push(r);
69
+ else
70
+ withheld.push({ rule: r, why });
71
+ }
72
+ if (withheld.length > 0 || exact.length === 0)
73
+ return { deliverable: {}, withheld };
74
+ return { deliverable: { toolDeny: exact }, withheld };
75
+ }
76
+ export function sessionPolicyWithheldNotice(why) {
77
+ return typeof why === 'string' && Object.hasOwn(WITHHELD_NOTICE, why)
78
+ ? WITHHELD_NOTICE[why]
79
+ : 'Not written to this session’s rule record.';
80
+ }
@@ -1,5 +1,6 @@
1
1
  import { type GateCurrentPending, type HitlFailureStage, type HitlSafetyCode, type HitlClientLike, type DecideOperationObserver } from './hitlBridge.js';
2
2
  import { type RuleStoreUnreadableKind } from '../gateVocabulary.js';
3
+ import { type ApprovalMandateWord } from '../gateVocabulary.js';
3
4
  import { type DenyAttributionRefusalDetail } from '../wireRefusalCopy.js';
4
5
  import { type DecideReceiptView } from '../decideReceipt.js';
5
6
  import { RULE_OFFER_MATCHES, RULE_OFFER_BATCH_MEMBER_KINDS, RULE_OFFER_UNCOVERED_REASONS, RULE_OFFERS_ABSENCE_REASONS, DENIAL_LIMIT_KINDS } from '@sema-agent/sdk';
@@ -137,6 +138,7 @@ export interface ApprovalCardRequest {
137
138
  receivedAtMs?: number;
138
139
  ruleOffersAbsence?: string;
139
140
  mandated?: true;
141
+ mandate?: ApprovalMandateWord;
140
142
  denialLimitFallback?: DenialLimitFallback;
141
143
  origin?: string;
142
144
  ruleStoreUnreadable?: RuleStoreUnreadableKind;
@@ -176,6 +178,7 @@ export interface ToolApprovalFrame {
176
178
  ruleEvidence?: unknown;
177
179
  requiresRealApproval?: true;
178
180
  mandated?: true;
181
+ mandate?: string;
179
182
  origin?: string;
180
183
  inputHasBidi?: true;
181
184
  parked?: true;
@@ -194,11 +197,16 @@ export interface ToolApprovalDelegation {
194
197
  depth: number;
195
198
  agentName?: string;
196
199
  }
197
- export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "ruleOffers", "ruleOffersAbsence", "denialLimitFallback", "ruleStoreUnreadable", "readRootCandidate", "mandated", "origin", "inputHasBidi", "parked", "persistedRuleShadowed", "probeCause", "ruleEvidence", "requiresRealApproval", "expiresInMs", "expiresAtMs", "serverNowMs", "delegation", "outcome"];
198
- export declare function approvalIsMandated(v: {
199
- mandated?: true;
200
- ruleOffersAbsence?: string;
201
- } | undefined | null): boolean;
200
+ export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "ruleOffers", "ruleOffersAbsence", "denialLimitFallback", "ruleStoreUnreadable", "readRootCandidate", "mandated", "mandate", "origin", "inputHasBidi", "parked", "persistedRuleShadowed", "probeCause", "ruleEvidence", "requiresRealApproval", "expiresInMs", "expiresAtMs", "serverNowMs", "delegation", "outcome"];
201
+ export interface ApprovalMandatedFacts {
202
+ readonly mandated?: true;
203
+ readonly ruleOffersAbsence?: string;
204
+ readonly mandate?: string;
205
+ }
206
+ export declare function approvalIsMandated(v: ApprovalMandatedFacts | undefined | null): boolean;
207
+ export declare function readApprovalMandate(v: {
208
+ readonly mandate?: string;
209
+ } | undefined | null): ApprovalMandateWord | undefined;
202
210
  export declare function isFromSubagent(frame: ToolApprovalFrame): boolean;
203
211
  export type ToolApprovalRespondDecision = 'allow' | 'allow_session' | 'deny';
204
212
  export interface RespondToolApprovalOpts {
@@ -1,11 +1,13 @@
1
1
  import { DEFAULT_DENY_REASON, DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, denyReasonForWire, findPendingForTask, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
2
2
  import { askParkRowArm } from './askParkRowRouting.js';
3
3
  import { hostLog } from '../host.js';
4
+ import { readOwnNonEmptyString } from '../ownKey.js';
4
5
  import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
5
6
  import { readEngineActiveBgTasks } from '../fleet/fleetLedger.js';
6
7
  import { observeCancelByDeny, surfaceRememberNotApplied, surfaceEditNotForwarded, surfaceEditRefusedOnBlindAsk, surfaceRuleArmNotSent, surfaceRuleArmRejected } from './hitlHostSurface.js';
7
8
  import { approvalCallKey, liveFrameCallKey } from './gateIdentity.js';
8
9
  import { isRuleStoreUnreadableKind } from '../gateVocabulary.js';
10
+ import { isApprovalMandateWord } from '../gateVocabulary.js';
9
11
  import { denyAttributionRefusalFromError } from '../wireRefusalCopy.js';
10
12
  import { decideFailureNote } from '../decideFailureNote.js';
11
13
  import { readDecideReceipt } from '../decideReceipt.js';
@@ -205,6 +207,8 @@ export async function surfaceFsApprovalAndDecide(deps, taskId, argsByCall, signa
205
207
  ...(pending.governanceForced === true ? { governanceForced: true } : {}),
206
208
  ...(pending.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
207
209
  ...(pending.mandated === true ? { mandated: true } : {}),
210
+ ...((w) => (w !== undefined ? { mandate: w } : {}))(readApprovalMandate(pending)),
211
+ ...((o) => (o !== undefined ? { origin: o } : {}))(readOwnNonEmptyString(pending, 'origin')),
208
212
  ...(pending.gateKind === 'irreversible_ask' ? { irreversibleParkGate: true } : {}),
209
213
  ...(pending.hasBidiControls === true ? { hasBidiControls: true } : {}),
210
214
  ...(ruleOffersReadOnly !== undefined ? { ruleOffersReadOnly } : {}),
@@ -379,6 +383,7 @@ export const TOOL_APPROVAL_FRAME_KEYS_MIRROR = [
379
383
  'ruleStoreUnreadable',
380
384
  'readRootCandidate',
381
385
  'mandated',
386
+ 'mandate',
382
387
  'origin',
383
388
  'inputHasBidi',
384
389
  'parked',
@@ -400,7 +405,15 @@ export function approvalIsMandated(v) {
400
405
  return false;
401
406
  if (v.mandated === true)
402
407
  return true;
403
- return v.ruleOffersAbsence === MANDATED_ABSENCE_WORD;
408
+ if (v.ruleOffersAbsence === MANDATED_ABSENCE_WORD)
409
+ return true;
410
+ return readApprovalMandate(v) !== undefined;
411
+ }
412
+ export function readApprovalMandate(v) {
413
+ if (typeof v !== 'object' || v === null || !Object.hasOwn(v, 'mandate'))
414
+ return undefined;
415
+ const raw = v.mandate;
416
+ return isApprovalMandateWord(raw) ? raw : undefined;
404
417
  }
405
418
  function isToolApprovalDelegation(v) {
406
419
  if (v === null || typeof v !== 'object')
@@ -828,6 +841,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
828
841
  ...(isWireRecordCarrier(frame.ruleEvidence) ? { ruleEvidence: frame.ruleEvidence } : {}),
829
842
  ...(frame.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
830
843
  ...(frame.mandated === true ? { mandated: true } : {}),
844
+ ...((w) => (w !== undefined ? { mandate: w } : {}))(readApprovalMandate(frame)),
831
845
  ...(() => { const rc = readReadRootCandidate(frame.readRootCandidate); return rc !== undefined ? { readRootCandidate: rc } : {}; })(),
832
846
  ...(frame.inputHasBidi === true ? { inputHasBidi: true } : {}),
833
847
  ...(typeof frame.ruleOffersAbsence === 'string' && frame.ruleOffersAbsence !== ''
@@ -835,7 +849,7 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
835
849
  : {}),
836
850
  ...(denialLimitFallback !== undefined ? { denialLimitFallback } : {}),
837
851
  ...((k) => (k !== undefined ? { ruleStoreUnreadable: k } : {}))(readRuleStoreUnreadable(frame.ruleStoreUnreadable)),
838
- ...(typeof frame.origin === 'string' && frame.origin !== '' ? { origin: frame.origin } : {}),
852
+ ...((o) => (o !== undefined ? { origin: o } : {}))(readOwnNonEmptyString(frame, 'origin')),
839
853
  });
840
854
  if (argsNotRealInput && card.kind === 'allow' && card.updatedInput !== undefined) {
841
855
  hostLog('error', `liveToolApprovalWire: ${approvalId} card returned an edited approval on a card whose arguments are not the tool's real input (${lane?.argsUnavailable === true ? 'args-unavailable ask' : 'arguments omitted or absent on the frame'}) — nothing sent (the ask stays pending)`);
@@ -1,8 +1,13 @@
1
+ import { type HooksWirePlan, type HooksWirePlanOptions } from './pluginHooksWire.js';
1
2
  import { type HookFailureNotice } from './notifications.js';
2
3
  import type { WireHooksConfig } from './finalVerifyWire.js';
3
4
  export { GOAL_STOP_HOOK_WIRE_ENV, CC_STOP_SEMANTICS_MIN_SERVER, ccStopSemanticsFromVersion, engineCcStopSemantics, isGoalStopHookWireArmed, setWireSessionStopHook, getWireSessionStopHook, getWireSessionStopHookCcSemantics, buildGoalStopHookPrompt, } from './goalStopHook.js';
4
5
  export type { WireHooksConfig };
5
- export declare function hooksForWire(): WireHooksConfig | undefined;
6
+ export interface HooksForWireOptions {
7
+ readonly plan?: HooksWirePlan;
8
+ }
9
+ export declare function hooksForWire(opts?: HooksForWireOptions): WireHooksConfig | undefined;
10
+ export declare function hooksWirePlan(opts?: HooksWirePlanOptions): HooksWirePlan;
6
11
  export declare function __resetHooksWireCapsForTests(): void;
7
12
  export declare function fleetHookFailureNoticeCapable(baseUrl: string | undefined): boolean;
8
13
  export declare const HOOK_FAILURE_WARN_PREFIX = "Hook error";