@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.
- package/CHANGELOG.md +83 -0
- package/README.md +10 -5
- package/dist/adapt/arms.js +1 -0
- package/dist/adapter/downstream/terminalToSdkResult.js +3 -3
- package/dist/adapter/runStream.js +19 -2
- package/dist/displayUntrusted.d.ts +24 -0
- package/dist/displayUntrusted.js +1437 -0
- package/dist/engineAgentAbsence.d.ts +52 -0
- package/dist/engineAgentAbsence.js +131 -0
- package/dist/engineNoticeCodes.js +4 -0
- package/dist/fleetTaskDesc.js +3 -3
- package/dist/gateVocabulary.d.ts +4 -0
- package/dist/gateVocabulary.js +29 -1
- package/dist/hitl/approvalDecisionNoteAudit.js +4 -2
- package/dist/hitl/askSurvivesPosture.d.ts +15 -0
- package/dist/hitl/askSurvivesPosture.js +31 -0
- package/dist/hitl/sessionPolicyDeliverable.d.ts +14 -0
- package/dist/hitl/sessionPolicyDeliverable.js +80 -0
- package/dist/hitl/toolApprovalWire.d.ts +13 -5
- package/dist/hitl/toolApprovalWire.js +16 -2
- package/dist/hooksWireCaps.d.ts +6 -1
- package/dist/hooksWireCaps.js +194 -13
- package/dist/host.d.ts +24 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/ownKey.d.ts +1 -0
- package/dist/ownKey.js +6 -0
- package/dist/peerFrames.js +2 -1
- package/dist/pluginHooksWire.d.ts +92 -0
- package/dist/pluginHooksWire.js +429 -0
- package/dist/subagent/engineSubagentResume.js +2 -1
- package/docs/INTEGRATION-CLIENTS.md +552 -13
- package/package.json +2 -2
|
@@ -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',
|
package/dist/fleetTaskDesc.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { applyDisplayFace, LEGACY_ESCAPE_FACE, LEGACY_LABEL_FACE } from './displayUntrusted.js';
|
|
2
2
|
export function escapeDisplayControlChars(s) {
|
|
3
|
-
return s
|
|
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
|
|
22
|
+
return applyDisplayFace(s, LEGACY_LABEL_FACE);
|
|
23
23
|
}
|
|
24
24
|
export function taskDescFromName(name, agentType, agentName) {
|
|
25
25
|
if (!name || !agentType)
|
package/dist/gateVocabulary.d.ts
CHANGED
|
@@ -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;
|
package/dist/gateVocabulary.js
CHANGED
|
@@ -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
|
|
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
|
|
199
|
-
mandated?: true;
|
|
200
|
-
ruleOffersAbsence?: string;
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
...(
|
|
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)`);
|
package/dist/hooksWireCaps.d.ts
CHANGED
|
@@ -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
|
|
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";
|