@sema-agent/client-core 0.83.4 → 0.83.6
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 +131 -0
- package/README.md +17 -16
- package/dist/adapter/activeRunSelfHeal.d.ts +3 -0
- package/dist/adapter/activeRunSelfHeal.js +39 -12
- package/dist/compensations.js +1 -1
- package/dist/displayUntrusted.d.ts +13 -0
- package/dist/displayUntrusted.js +751 -125
- package/dist/hitl/sessionPolicyDeliverable.d.ts +2 -1
- package/dist/hitl/sessionPolicyDeliverable.js +54 -4
- package/dist/hooksWireCaps.js +78 -8
- package/dist/host.d.ts +2 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/pluginHooksWire.d.ts +9 -0
- package/dist/pluginHooksWire.js +119 -0
- package/dist/request/taskRequest.js +1 -1
- package/docs/INTEGRATION-CLIENTS.md +396 -66
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { SessionPermissionRules } from '@sema-agent/sdk';
|
|
2
|
-
export type SessionPolicyWithheldWhy = 'ask_no_bucket' | 'qualified_deny' | 'peer_wide' | 'wildcard' | 'not_a_tool_name';
|
|
2
|
+
export type SessionPolicyWithheldWhy = 'ask_no_bucket' | 'qualified_deny' | 'peer_wide' | 'wildcard' | 'not_a_tool_name' | 'legacy_tool_name';
|
|
3
3
|
export declare const SESSION_POLICY_WITHHELD_WHY: readonly SessionPolicyWithheldWhy[];
|
|
4
4
|
export interface SessionPolicyWithheldRule {
|
|
5
5
|
readonly rule: string;
|
|
6
6
|
readonly why: SessionPolicyWithheldWhy;
|
|
7
|
+
readonly currentName?: string;
|
|
7
8
|
}
|
|
8
9
|
export interface SessionPolicyDeliverability {
|
|
9
10
|
readonly deliverable: SessionPermissionRules;
|
|
@@ -1,16 +1,60 @@
|
|
|
1
|
+
const COVERING_NAMESPACE_PREFIXES = ['mcp__', 'a2a__'];
|
|
2
|
+
const ENGINE_RETIRED_TOOL_NAMES = new Map([
|
|
3
|
+
['read_file', 'Read'],
|
|
4
|
+
['edit_file', 'Edit'],
|
|
5
|
+
['write_file', 'Write'],
|
|
6
|
+
['bash', 'Bash'],
|
|
7
|
+
['bash_readonly', null],
|
|
8
|
+
['glob', 'Glob'],
|
|
9
|
+
['grep', 'Grep'],
|
|
10
|
+
['web_fetch', 'WebFetch'],
|
|
11
|
+
['tool_search', 'ToolSearch'],
|
|
12
|
+
['skill', 'Skill'],
|
|
13
|
+
['present_plan', 'ExitPlanMode'],
|
|
14
|
+
['enter_plan_mode', 'EnterPlanMode'],
|
|
15
|
+
['spawn_subagent', 'Agent'],
|
|
16
|
+
['CronCancel', 'CronDelete'],
|
|
17
|
+
['lsp', 'LSP'],
|
|
18
|
+
['submit_output', 'StructuredOutput'],
|
|
19
|
+
['repo_map', 'RepoMap'],
|
|
20
|
+
['read_tool_result', 'ReadToolResult'],
|
|
21
|
+
['report_blocked', 'ReportBlocked'],
|
|
22
|
+
['declare_done', 'DeclareDone'],
|
|
23
|
+
['run_workflow', 'Workflow'],
|
|
24
|
+
['RunWorkflow', 'Workflow'],
|
|
25
|
+
['Task', 'Agent'],
|
|
26
|
+
['BashOutput', 'TaskOutput'],
|
|
27
|
+
['AgentOutputTool', 'TaskOutput'],
|
|
28
|
+
['BashOutputTool', 'TaskOutput'],
|
|
29
|
+
['AgentOutput', 'TaskOutput'],
|
|
30
|
+
['WorkflowStatus', null],
|
|
31
|
+
['KillShell', 'TaskStop'],
|
|
32
|
+
['KillBash', 'TaskStop'],
|
|
33
|
+
['ListMcpResources', 'ListMcpResourcesTool'],
|
|
34
|
+
['ReadMcpResource', 'ReadMcpResourceTool'],
|
|
35
|
+
['ReadMcpResourceDir', 'ReadMcpResourceDirTool'],
|
|
36
|
+
['MultiEdit', null],
|
|
37
|
+
['Fork', null],
|
|
38
|
+
['Sleep', null],
|
|
39
|
+
['Remember', null],
|
|
40
|
+
['remember', null],
|
|
41
|
+
['Recall', null],
|
|
42
|
+
['recall', null],
|
|
43
|
+
]);
|
|
44
|
+
const PREFIXES_IN_PROSE = COVERING_NAMESPACE_PREFIXES.map((prefix) => `"${prefix}"`).join(' or ');
|
|
1
45
|
const WITHHELD_NOTICE = {
|
|
2
46
|
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
47
|
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
|
|
48
|
+
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.',
|
|
5
49
|
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
50
|
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.',
|
|
51
|
+
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}.`,
|
|
7
52
|
};
|
|
8
53
|
export const SESSION_POLICY_WITHHELD_WHY = Object.freeze(Object.keys(WITHHELD_NOTICE));
|
|
9
54
|
const MAX_RULES = 10_000;
|
|
10
55
|
function unreadable() {
|
|
11
56
|
return { deliverable: {}, withheld: [], unreadable: true };
|
|
12
57
|
}
|
|
13
|
-
const COVERING_NAMESPACE_PREFIXES = ['mcp__', 'a2a__'];
|
|
14
58
|
function withheldWhyOf(rule) {
|
|
15
59
|
if (rule.length === 0)
|
|
16
60
|
return 'not_a_tool_name';
|
|
@@ -27,6 +71,10 @@ function withheldWhyOf(rule) {
|
|
|
27
71
|
}
|
|
28
72
|
if (rule.trim() !== rule)
|
|
29
73
|
return 'not_a_tool_name';
|
|
74
|
+
if (ENGINE_RETIRED_TOOL_NAMES.has(rule))
|
|
75
|
+
return 'legacy_tool_name';
|
|
76
|
+
if (rule.includes('__') && !COVERING_NAMESPACE_PREFIXES.some((prefix) => rule.startsWith(prefix)))
|
|
77
|
+
return 'legacy_tool_name';
|
|
30
78
|
return undefined;
|
|
31
79
|
}
|
|
32
80
|
export function sessionPolicyDeliverable(behavior, rules) {
|
|
@@ -66,8 +114,10 @@ export function sessionPolicyDeliverable(behavior, rules) {
|
|
|
66
114
|
const why = withheldWhyOf(r);
|
|
67
115
|
if (why === undefined)
|
|
68
116
|
exact.push(r);
|
|
69
|
-
else
|
|
70
|
-
|
|
117
|
+
else {
|
|
118
|
+
const currentName = why === 'legacy_tool_name' ? ENGINE_RETIRED_TOOL_NAMES.get(r) : undefined;
|
|
119
|
+
withheld.push(typeof currentName === 'string' ? { rule: r, why, currentName } : { rule: r, why });
|
|
120
|
+
}
|
|
71
121
|
}
|
|
72
122
|
if (withheld.length > 0 || exact.length === 0)
|
|
73
123
|
return { deliverable: {}, withheld };
|
package/dist/hooksWireCaps.js
CHANGED
|
@@ -2,7 +2,7 @@ import { engineCapNestedTrue, engineCapState } from './engineCapsCache.js';
|
|
|
2
2
|
import { displayUntrusted } from './displayUntrusted.js';
|
|
3
3
|
import { collapseLabel } from './fleetTaskDesc.js';
|
|
4
4
|
import { hostLog, hostSettings } from './host.js';
|
|
5
|
-
import { decidePluginHooks, filterSettingsHookGroups, occupancyOf, settingsHookDropText, } from './pluginHooksWire.js';
|
|
5
|
+
import { decidePluginHooks, dedupeSettingsHookGroups, filterSettingsHookGroups, occupancyOf, settingsHookDropText, } from './pluginHooksWire.js';
|
|
6
6
|
import { MAX_HOOK_NOTICE_TEXT_CHARS } from './notifications.js';
|
|
7
7
|
import { goalStopHookMatcher, __resetGoalStopHookForTests } from './goalStopHook.js';
|
|
8
8
|
export { GOAL_STOP_HOOK_WIRE_ENV, CC_STOP_SEMANTICS_MIN_SERVER, ccStopSemanticsFromVersion, engineCcStopSemantics, isGoalStopHookWireArmed, setWireSessionStopHook, getWireSessionStopHook, getWireSessionStopHookCcSemantics, buildGoalStopHookPrompt, } from './goalStopHook.js';
|
|
@@ -37,6 +37,40 @@ function nonManagedDisableAllHooks(settings) {
|
|
|
37
37
|
}
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
|
+
const HOOKS_STARTUP_MODES = ['safe', 'bare'];
|
|
41
|
+
const _startupModesPin = [true, true];
|
|
42
|
+
void _startupModesPin;
|
|
43
|
+
function describeStartupModeValue(v) {
|
|
44
|
+
if (v === null)
|
|
45
|
+
return 'null';
|
|
46
|
+
if (typeof v === 'string')
|
|
47
|
+
return JSON.stringify(v.length > 40 ? `${v.slice(0, 40)}…` : v);
|
|
48
|
+
return typeof v;
|
|
49
|
+
}
|
|
50
|
+
function startupModeWord(settings) {
|
|
51
|
+
let v;
|
|
52
|
+
try {
|
|
53
|
+
const member = settings.hooksStartupMode;
|
|
54
|
+
if (member === undefined)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (typeof member !== 'function') {
|
|
57
|
+
hostLog('debug', `hooksWireCaps: hooksStartupMode is ${describeStartupModeValue(member)}, not a method — treated as neither safe nor bare mode (settings hooks not narrowed)`);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
v = member.call(settings);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
hostLog('debug', `hooksWireCaps: hooksStartupMode() threw — treated as neither safe nor bare mode (settings hooks not narrowed): ${String(err)}`);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (v === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
const word = HOOKS_STARTUP_MODES.find((w) => w === v);
|
|
69
|
+
if (word !== undefined)
|
|
70
|
+
return word;
|
|
71
|
+
hostLog('debug', `hooksWireCaps: hooksStartupMode() returned ${describeStartupModeValue(v)}, which is neither 'safe' nor 'bare' — treated as neither mode (settings hooks not narrowed)`);
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
40
74
|
function hooksOfSource(source) {
|
|
41
75
|
try {
|
|
42
76
|
const s = hostSettings()?.getSettingsForSource(source);
|
|
@@ -203,8 +237,27 @@ function buildHooksWirePlan(engine, shellEvents, withPlugins) {
|
|
|
203
237
|
hostLog('debug', 'hooksWireCaps: disableAllHooks (merged, non-managed) — only policySettings hooks projected to the engine');
|
|
204
238
|
}
|
|
205
239
|
const managedOnly = managedByPolicy || disabledByNonManaged;
|
|
206
|
-
const
|
|
240
|
+
const read = withPlugins ? readPluginHooks(settings, true) : undefined;
|
|
241
|
+
let oldStartupBit;
|
|
242
|
+
let pluginDataThrew = false;
|
|
243
|
+
if (read?.reading !== undefined) {
|
|
244
|
+
const reading = read.reading;
|
|
245
|
+
const bit = guardPluginTraversal(() => reading.suppressedByStartupMode === true);
|
|
246
|
+
if (bit === undefined)
|
|
247
|
+
pluginDataThrew = true;
|
|
248
|
+
else
|
|
249
|
+
oldStartupBit = bit;
|
|
250
|
+
}
|
|
251
|
+
const startupMode = startupModeWord(settings) ?? (oldStartupBit === true ? 'safe' : undefined);
|
|
252
|
+
if (startupMode === 'bare') {
|
|
253
|
+
hostLog('debug', 'hooksWireCaps: bare mode reported by the host — only policySettings hooks projected to the engine (same as safe mode)');
|
|
254
|
+
}
|
|
255
|
+
else if (startupMode === 'safe') {
|
|
256
|
+
hostLog('debug', 'hooksWireCaps: safe mode reported by the host — only policySettings hooks projected to the engine');
|
|
257
|
+
}
|
|
258
|
+
const sources = managedOnly || startupMode !== undefined ? ['policySettings'] : ALL_HOOK_SOURCES;
|
|
207
259
|
const merged = {};
|
|
260
|
+
const sourced = new Map();
|
|
208
261
|
for (const source of sources) {
|
|
209
262
|
const hooks = hooksOfSource(source);
|
|
210
263
|
if (!hooks)
|
|
@@ -218,8 +271,20 @@ function buildHooksWirePlan(engine, shellEvents, withPlugins) {
|
|
|
218
271
|
if (kept.groups.length === 0)
|
|
219
272
|
continue;
|
|
220
273
|
merged[event] = [...(merged[event] ?? []), ...kept.groups];
|
|
274
|
+
const tagged = sourced.get(event) ?? [];
|
|
275
|
+
for (const group of kept.groups)
|
|
276
|
+
tagged.push({ source, group });
|
|
277
|
+
sourced.set(event, tagged);
|
|
221
278
|
}
|
|
222
279
|
}
|
|
280
|
+
let settingsDeduped = 0;
|
|
281
|
+
for (const [event, groups] of sourced) {
|
|
282
|
+
const d = dedupeSettingsHookGroups(groups);
|
|
283
|
+
if (d.removed === 0)
|
|
284
|
+
continue;
|
|
285
|
+
merged[event] = d.groups;
|
|
286
|
+
settingsDeduped += d.removed;
|
|
287
|
+
}
|
|
223
288
|
const goalMatcher = goalStopHookMatcher(managedOnly);
|
|
224
289
|
if (goalMatcher !== null) {
|
|
225
290
|
merged['Stop'] = [...(merged['Stop'] ?? []), goalMatcher];
|
|
@@ -228,6 +293,10 @@ function buildHooksWirePlan(engine, shellEvents, withPlugins) {
|
|
|
228
293
|
hostLog('debug', `hooksWireCaps: ${settingsDropped.length} settings hook entr${settingsDropped.length === 1 ? 'y' : 'ies'} not sent to the engine ` +
|
|
229
294
|
`(${[...new Set(settingsDropped.map((d) => d.reason))].join(', ')}) — see hooksWirePlan().settingsDropped`);
|
|
230
295
|
}
|
|
296
|
+
if (settingsDeduped > 0) {
|
|
297
|
+
hostLog('debug', `hooksWireCaps: ${settingsDeduped} settings hook entr${settingsDeduped === 1 ? 'y appears' : 'ies appear'} more than once ` +
|
|
298
|
+
'(same event, matcher group and hook) — sent once, at the first position, with the fields of the highest-precedence source');
|
|
299
|
+
}
|
|
231
300
|
if (settingsStripped > 0) {
|
|
232
301
|
hostLog('debug', `hooksWireCaps: ${settingsStripped} settings hook entr${settingsStripped === 1 ? 'y' : 'ies'} carried a plugin key — ` +
|
|
233
302
|
'stripped before sending (plugin context is only minted for entries from the plugin reader)');
|
|
@@ -250,18 +319,19 @@ function buildHooksWirePlan(engine, shellEvents, withPlugins) {
|
|
|
250
319
|
}
|
|
251
320
|
return planOf('absent', wireOf());
|
|
252
321
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
322
|
+
if (read?.reading === undefined)
|
|
323
|
+
return planOf(read?.state ?? 'absent', wireOf());
|
|
324
|
+
if (pluginDataThrew)
|
|
325
|
+
return planOf('failed', wireOf());
|
|
256
326
|
const reading = read.reading;
|
|
257
327
|
const occupied = occupancyOf(merged);
|
|
258
328
|
const d = guardPluginTraversal(() => {
|
|
259
329
|
const plugins = reading.plugins;
|
|
260
|
-
const
|
|
261
|
-
const managedOnlyPlugins = !
|
|
330
|
+
const startup = startupMode !== undefined;
|
|
331
|
+
const managedOnlyPlugins = !startup && plugins.length > 0 && (policy?.allowManagedHooksOnly === true || mergedDisable());
|
|
262
332
|
return decidePluginHooks({
|
|
263
333
|
plugins,
|
|
264
|
-
exclusion:
|
|
334
|
+
exclusion: startup ? 'startup_mode' : undefined,
|
|
265
335
|
managedOnly: managedOnlyPlugins,
|
|
266
336
|
engine,
|
|
267
337
|
shellHookEvents: shellEvents,
|
package/dist/host.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ export interface SettingsPort {
|
|
|
7
7
|
shouldSkipHookDueToTrust(hook?: unknown): boolean;
|
|
8
8
|
mergedDisableAllHooks?(): boolean | undefined;
|
|
9
9
|
enabledPluginHooks?(): PluginHooksReading;
|
|
10
|
+
hooksStartupMode?(): HooksStartupMode | undefined;
|
|
10
11
|
}
|
|
12
|
+
export type HooksStartupMode = 'safe' | 'bare';
|
|
11
13
|
export type PluginHookOptionValue = string | number | boolean;
|
|
12
14
|
export interface PluginHookSensitiveOption {
|
|
13
15
|
readonly sensitive: true;
|
package/dist/index.d.ts
CHANGED
|
@@ -57,8 +57,8 @@ export * from './modelCapabilityProbe.js';
|
|
|
57
57
|
export * from './classifierStatus.js';
|
|
58
58
|
export * from './engineToolLabelStore.js';
|
|
59
59
|
export * from './fleetTaskDesc.js';
|
|
60
|
-
export { displayUntrusted } from './displayUntrusted.js';
|
|
61
|
-
export type { DisplayUntrustedOptions, DisplayMark } from './displayUntrusted.js';
|
|
60
|
+
export { displayUntrusted, displayUntrustedMarks } from './displayUntrusted.js';
|
|
61
|
+
export type { DisplayUntrustedOptions, DisplayMark, DisplayMarkSpan, DisplayUntrustedMarked } from './displayUntrusted.js';
|
|
62
62
|
export type * from './types/engineState.js';
|
|
63
63
|
export * from './hostEnv.js';
|
|
64
64
|
export * from './unrefTimer.js';
|
package/dist/index.js
CHANGED
|
@@ -57,7 +57,7 @@ export * from './modelCapabilityProbe.js';
|
|
|
57
57
|
export * from './classifierStatus.js';
|
|
58
58
|
export * from './engineToolLabelStore.js';
|
|
59
59
|
export * from './fleetTaskDesc.js';
|
|
60
|
-
export { displayUntrusted } from './displayUntrusted.js';
|
|
60
|
+
export { displayUntrusted, displayUntrustedMarks } from './displayUntrusted.js';
|
|
61
61
|
export * from './hostEnv.js';
|
|
62
62
|
export * from './unrefTimer.js';
|
|
63
63
|
export * from './controlRouter.js';
|
|
@@ -84,6 +84,15 @@ export interface SettingsHookFilterResult {
|
|
|
84
84
|
readonly drops: SettingsHookDrop[];
|
|
85
85
|
readonly stripped: number;
|
|
86
86
|
}
|
|
87
|
+
export interface SourcedSettingsHookGroup {
|
|
88
|
+
readonly source: HookSettingsSource;
|
|
89
|
+
readonly group: WireHookGroup;
|
|
90
|
+
}
|
|
91
|
+
export declare function dedupeSettingsHookGroups(groups: readonly SourcedSettingsHookGroup[]): SettingsHookDedupResult;
|
|
92
|
+
export interface SettingsHookDedupResult {
|
|
93
|
+
readonly groups: WireHookGroup[];
|
|
94
|
+
readonly removed: number;
|
|
95
|
+
}
|
|
87
96
|
export declare function pluginHookVerdictOf(plan: HooksWirePlan, pluginId: string, event: string, groupIndex: number, hookIndex: number): PluginHookVerdict | undefined;
|
|
88
97
|
export declare function pluginHookReasonText(reason: PluginHookReason): string;
|
|
89
98
|
export declare function settingsHookDropText(reason: SettingsHookDropReason): string;
|
package/dist/pluginHooksWire.js
CHANGED
|
@@ -305,6 +305,125 @@ export function filterSettingsHookGroups(source, event, groups, execFormCapable,
|
|
|
305
305
|
});
|
|
306
306
|
return { groups: out, drops, stripped };
|
|
307
307
|
}
|
|
308
|
+
function settingsHookEntryKey(entry) {
|
|
309
|
+
if (!isRecord(entry) || typeof entry.type !== 'string')
|
|
310
|
+
return undefined;
|
|
311
|
+
const cond = entry.if;
|
|
312
|
+
if (cond !== undefined && typeof cond !== 'string')
|
|
313
|
+
return undefined;
|
|
314
|
+
const ifKey = cond ?? '';
|
|
315
|
+
const str = (v) => typeof v === 'string';
|
|
316
|
+
const shell = entry.shell;
|
|
317
|
+
try {
|
|
318
|
+
switch (entry.type) {
|
|
319
|
+
case 'command':
|
|
320
|
+
if (!str(entry.command) || (shell !== undefined && !str(shell)))
|
|
321
|
+
return undefined;
|
|
322
|
+
return JSON.stringify(['command', shell ?? null, entry.command, entry.args ?? null, ifKey]);
|
|
323
|
+
case 'prompt':
|
|
324
|
+
case 'agent':
|
|
325
|
+
return str(entry.prompt) ? JSON.stringify([entry.type, entry.prompt, ifKey]) : undefined;
|
|
326
|
+
case 'http':
|
|
327
|
+
return str(entry.url) ? JSON.stringify(['http', entry.url, ifKey]) : undefined;
|
|
328
|
+
case 'mcp_tool':
|
|
329
|
+
return str(entry.server) && str(entry.tool)
|
|
330
|
+
? JSON.stringify(['mcp_tool', entry.server, entry.tool, entry.input ?? {}, ifKey])
|
|
331
|
+
: undefined;
|
|
332
|
+
case 'script': {
|
|
333
|
+
const script = entry.script;
|
|
334
|
+
const file = entry.file;
|
|
335
|
+
if ((script !== undefined && !str(script)) || (file !== undefined && !str(file)) || (script === undefined && file === undefined))
|
|
336
|
+
return undefined;
|
|
337
|
+
return JSON.stringify(['script', script ?? null, file ?? null, ifKey]);
|
|
338
|
+
}
|
|
339
|
+
default:
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function ccMergeRank(source) {
|
|
348
|
+
switch (source) {
|
|
349
|
+
case 'userSettings':
|
|
350
|
+
return 0;
|
|
351
|
+
case 'projectSettings':
|
|
352
|
+
return 1;
|
|
353
|
+
case 'localSettings':
|
|
354
|
+
return 2;
|
|
355
|
+
case 'flagSettings':
|
|
356
|
+
return 3;
|
|
357
|
+
case 'policySettings':
|
|
358
|
+
return 4;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
export function dedupeSettingsHookGroups(groups) {
|
|
362
|
+
const keysOf = [];
|
|
363
|
+
const winner = new Map();
|
|
364
|
+
for (const { source, group } of groups) {
|
|
365
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) {
|
|
366
|
+
keysOf.push([]);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
let groupKey;
|
|
370
|
+
try {
|
|
371
|
+
const { hooks: _hooks, ...rest } = group;
|
|
372
|
+
void _hooks;
|
|
373
|
+
groupKey = JSON.stringify(rest);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
groupKey = undefined;
|
|
377
|
+
}
|
|
378
|
+
const rank = ccMergeRank(source);
|
|
379
|
+
const keys = [];
|
|
380
|
+
for (const entry of group.hooks) {
|
|
381
|
+
const entryKey = groupKey === undefined ? undefined : settingsHookEntryKey(entry);
|
|
382
|
+
const key = entryKey === undefined ? undefined : `${groupKey}\u0000${entryKey}`;
|
|
383
|
+
keys.push(key);
|
|
384
|
+
if (key === undefined)
|
|
385
|
+
continue;
|
|
386
|
+
const prev = winner.get(key);
|
|
387
|
+
if (prev === undefined || rank >= prev.rank)
|
|
388
|
+
winner.set(key, { entry, rank });
|
|
389
|
+
}
|
|
390
|
+
keysOf.push(keys);
|
|
391
|
+
}
|
|
392
|
+
const placed = new Set();
|
|
393
|
+
const out = [];
|
|
394
|
+
let removed = 0;
|
|
395
|
+
groups.forEach(({ group }, gi) => {
|
|
396
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) {
|
|
397
|
+
out.push(group);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const keys = keysOf[gi] ?? [];
|
|
401
|
+
const kept = [];
|
|
402
|
+
let changed = false;
|
|
403
|
+
group.hooks.forEach((entry, hi) => {
|
|
404
|
+
const key = keys[hi];
|
|
405
|
+
if (key === undefined) {
|
|
406
|
+
kept.push(entry);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (placed.has(key)) {
|
|
410
|
+
removed++;
|
|
411
|
+
changed = true;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
placed.add(key);
|
|
415
|
+
const win = winner.get(key)?.entry ?? entry;
|
|
416
|
+
if (win !== entry)
|
|
417
|
+
changed = true;
|
|
418
|
+
kept.push(win);
|
|
419
|
+
});
|
|
420
|
+
if (!changed)
|
|
421
|
+
out.push(group);
|
|
422
|
+
else if (kept.length > 0)
|
|
423
|
+
out.push({ ...group, hooks: kept });
|
|
424
|
+
});
|
|
425
|
+
return { groups: out, removed };
|
|
426
|
+
}
|
|
308
427
|
export function pluginHookVerdictOf(plan, pluginId, event, groupIndex, hookIndex) {
|
|
309
428
|
return plan.verdicts.find((v) => v.pluginId === pluginId && v.event === event && v.groupIndex === groupIndex && v.hookIndex === hookIndex);
|
|
310
429
|
}
|
|
@@ -19,7 +19,7 @@ export const REQUEST_FIELD_MATRIX = [
|
|
|
19
19
|
{ field: 'council', lanes: ['interactive', 'print'], live: true, why: 'code-review council 形开关(server 读 `=== true`;非 true 不 stamp=与缺席同义,发小的那个)', whyUtilityLane: '无座:场景键;side-channel 不走场景' },
|
|
20
20
|
{ field: 'debate', lanes: ['interactive', 'print'], live: true, why: 'council 的 debate 变体开关(server 读 `=== true`)', whyUtilityLane: '无座:场景键;side-channel 不走场景' },
|
|
21
21
|
{ field: 'rounds', lanes: ['interactive', 'print'], live: true, why: 'debate 轮数(server clamp [1,3],非有限数被 server 归 undefined——本层原样透传不预 clamp)', whyUtilityLane: '无座:场景键;side-channel 不走场景' },
|
|
22
|
-
{ field: 'lenses', lanes: ['interactive', 'print'], live: true, why: 'code-review
|
|
22
|
+
{ field: 'lenses', lanes: ['interactive', 'print'], live: true, why: 'code-review 镜头数(server 场景按数字读、非数字回 400;本层型面仍按旧形声明为字符串数组 —— 已知不一致,随该族退役处理)', whyUtilityLane: '无座:场景键;side-channel 不走场景' },
|
|
23
23
|
{ field: 'memoryCapture', lanes: ['interactive', 'print', 'utility'], live: true, why: '本会话永远不进长期记忆的**声明形**(server §12.4 body 键,单成员闭集 "off");每条车道都是提交面 ⇒ 都能声明。与 memoryWrite:false 不是一个轴(那是「本 run 只读平面」)。⚠️ parked 续跑腿结构性拿不到声明(决策/续跑端点从持久 body 重建 spec),那是引擎侧事实,不是本层的漏', whyUtilityLane: '有座:side-channel 提交是壳自己的一次性问答,**绝不该**进用户的长期记忆 —— 这一位正是那句声明,且它是收紧方向,带上去只会更严' },
|
|
24
24
|
{ field: 'memory', lanes: ['interactive', 'print', 'utility'], live: true, why: '这一次运行不挂记忆面的**请求级**开关(server §12.7 body 键,单成员闭集 "off");每条车道都是提交面 ⇒ 都能声明。与 memoryCapture(采集面)不是一个轴、可并存;与 memoryWrite:false(只读平面)也不是一个轴', whyUtilityLane: '有座:side-channel 提交不需要挂记忆面(不挂记忆段 / 索引 / 三只记忆工具),省 token 且零记忆写入;与 memoryCapture 不是一个轴,两条都带' },
|
|
25
25
|
{ field: 'approverPosture', lanes: ['interactive', 'print'], live: true, why: '宿主对这个 run 的**审批姿态声明**(「这个 run 的审批卡由我自己自动答」;单成员闭集 `"bypass"`)。🔴 它不授权任何东西、也不是一只门:引擎只把这句声明**渲**给模型(它自己的标记块,首个回合),没有任何裁决腿读它 —— 与 `permissionMode` 的 `bypassPermissions`(选门的形状)**不是一回事**,两键在请求体上各自原样过境、**互不派生**:宿主完全可以一边留着缺省权限模式、一边自己自动答这个 run 的审批卡。🔴 **声明它的唯一条件是「宿主确实自己自动答这个 run 的审批卡」,不许据此推导或改动 `permissionMode`** —— 拿这一位当理由去把门放宽,是把一句只渲给模型看的话读成了授权。宿主并不自动答卡却声明了它 ⇒ 那是对模型说了一句与实际不符的话,修法是**别声明**,不是去动门。加座的理由是**一个事实一处铸点**:同样一句话,要么由引擎在它自己的标记块里说,要么由壳把提醒形文本拼进本 turn 的输入 —— 后者在模型眼里是一段**没有引擎标记**的提醒形副本,按标记声明它该当**数据**处理并报出来。所以壳从此停拼,交这一位。两条用户车道都是提交面 ⇒ 都能声明。坏词由引擎响亮拒(单成员闭集,打错字不许被折成缺席),本层同律在构造期就拒(见 `refuseBadApproverPosture`)。⚠️ 上游的**提交体**与**引擎规格**两层都真读这一键,而装着的 sdk 型面尚未镜像它 ⇒ 本层的字面按提交体的形声明,不从 sdk 取型', whyUtilityLane: '无座:审批姿态是**用户**对他自己那条会话的声明,而 side-channel 是壳自己的提交 —— 把用户的 bypass 姿态带进一条他没参与的提交,等于替他重申一句他没说过的话;况且零工具面根本产不出审批卡' },
|