@gajae-code/coding-agent 0.16.4 → 0.16.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 +54 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/helpers.d.ts +33 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +4 -2
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +4 -3
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +8 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +9 -7
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +198 -14
- package/src/config/model-preset-registry.ts +115 -16
- package/src/config/model-registry.ts +16 -12
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +50 -62
- package/src/discovery/helpers.ts +297 -32
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +20 -2
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +80 -47
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +29 -9
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/session-state-lock.ts +51 -8
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +6 -1
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/tool-status-header.ts +3 -1
- package/src/modes/controllers/command-controller.ts +1 -0
- package/src/modes/controllers/event-controller.ts +3 -2
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/interactive-mode.ts +6 -5
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/manager.ts +20 -2
- package/src/runtime-mcp/pool.test.ts +20 -11
- package/src/sdk/broker/broker.ts +119 -28
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +387 -48
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +2 -2
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +50 -8
- package/src/sdk/host/session-runtime.ts +158 -51
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session.ts +155 -26
- package/src/sdk/transport/relay.ts +35 -13
- package/src/session/agent-session.ts +117 -52
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +49 -43
- package/src/setup/paseo/setup-deps.ts +1 -1
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/system-prompt.ts +25 -3
- package/src/task/executor.ts +5 -0
- package/src/task/index.ts +4 -0
- package/src/tools/bash.ts +28 -11
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +2 -0
- package/src/tools/job.ts +13 -2
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +15 -0
- package/src/tools/subagent.ts +3 -2
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/tools/ultragoal-ask-guard.ts +2 -2
|
@@ -2,12 +2,24 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { logger } from "@gajae-code/utils";
|
|
4
4
|
import { normalizePluginHook } from "../../hooks/normalize";
|
|
5
|
+
import {
|
|
6
|
+
compatibilityFunctionHookGrant,
|
|
7
|
+
type FunctionHookGrant,
|
|
8
|
+
functionHookGrantHash,
|
|
9
|
+
normalizeFunctionHookGrant,
|
|
10
|
+
} from "../extensions/function-hooks";
|
|
11
|
+
import { compileGjcPluginBundle } from "./compiler";
|
|
5
12
|
import { bundleIdentity } from "./lifecycle-reconciliation";
|
|
6
13
|
import { verifyImplementationHash } from "./metadata";
|
|
7
14
|
import { resolveWithinRoot } from "./paths";
|
|
8
15
|
import { loadEffectiveGjcPluginRegistry } from "./registry";
|
|
9
16
|
import { type SessionQuarantine, validateSessionBundles, verifyEntryHashes } from "./session-validation";
|
|
10
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
GjcPluginLoadError,
|
|
19
|
+
type GjcPluginRegistryEntry,
|
|
20
|
+
type GjcPluginScope,
|
|
21
|
+
type NormalizedHookSurface,
|
|
22
|
+
} from "./types";
|
|
11
23
|
|
|
12
24
|
/**
|
|
13
25
|
* Constrained plugin-hook loader.
|
|
@@ -21,10 +33,23 @@ import { GjcPluginLoadError, type GjcPluginRegistryEntry, type GjcPluginScope }
|
|
|
21
33
|
|
|
22
34
|
export interface ConstrainedPluginHook {
|
|
23
35
|
plugin: string;
|
|
36
|
+
scope?: GjcPluginScope;
|
|
37
|
+
extensionId?: string;
|
|
24
38
|
event: string;
|
|
25
39
|
target?: string;
|
|
26
40
|
phase?: "before" | "after";
|
|
27
|
-
handler: (...args:
|
|
41
|
+
handler: (...args: never[]) => unknown;
|
|
42
|
+
grant?: FunctionHookGrant;
|
|
43
|
+
provenance?: {
|
|
44
|
+
source: "plugin-bundle";
|
|
45
|
+
scope: GjcPluginScope;
|
|
46
|
+
plugin: string;
|
|
47
|
+
path: string;
|
|
48
|
+
extensionId: string;
|
|
49
|
+
activationGeneration?: number;
|
|
50
|
+
};
|
|
51
|
+
functionHook?: boolean;
|
|
52
|
+
activationGeneration?: number;
|
|
28
53
|
}
|
|
29
54
|
|
|
30
55
|
export interface ConstrainedHookLoadResult {
|
|
@@ -52,16 +77,143 @@ async function resolveConstrainedHookFile(root: string, relativePath: string): P
|
|
|
52
77
|
export interface DeclaredHook {
|
|
53
78
|
plugin: string;
|
|
54
79
|
scope: GjcPluginScope;
|
|
80
|
+
extensionId?: string;
|
|
55
81
|
event: string;
|
|
56
82
|
target?: string;
|
|
57
83
|
phase?: "before" | "after";
|
|
58
84
|
relativePath: string;
|
|
59
85
|
implementationHash?: string;
|
|
86
|
+
capabilities?: string[];
|
|
87
|
+
networkDestinations?: string[];
|
|
88
|
+
filesystemRoots?: string[];
|
|
89
|
+
capabilityHash?: string;
|
|
90
|
+
pluginRoot?: string;
|
|
91
|
+
functionHook?: boolean;
|
|
92
|
+
activationGeneration?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface PersistedHookMetadata {
|
|
96
|
+
grant: FunctionHookGrant;
|
|
97
|
+
functionHook: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sameValues(actual: readonly unknown[] | undefined, expected: readonly unknown[]): boolean {
|
|
101
|
+
return (
|
|
102
|
+
actual !== undefined &&
|
|
103
|
+
actual.length === expected.length &&
|
|
104
|
+
actual.every((value, index) => value === expected[index])
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function invalidHookMetadata(plugin: string, detail: string): never {
|
|
109
|
+
throw new GjcPluginLoadError(
|
|
110
|
+
"security_policy",
|
|
111
|
+
`GJC plugin hook "${plugin}" has inconsistent function-hook metadata: ${detail}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Registry hook metadata is attacker-editable between install and session
|
|
117
|
+
* startup. Legacy hooks have no grant metadata at all; every hook carrying a
|
|
118
|
+
* grant must instead carry the complete canonical grant, its hash, and the
|
|
119
|
+
* function-hook marker.
|
|
120
|
+
*/
|
|
121
|
+
function readPersistedHookMetadata(input: {
|
|
122
|
+
plugin: string;
|
|
123
|
+
event: string;
|
|
124
|
+
capabilities?: unknown;
|
|
125
|
+
networkDestinations?: unknown;
|
|
126
|
+
filesystemRoots?: unknown;
|
|
127
|
+
capabilityHash?: unknown;
|
|
128
|
+
functionHook?: unknown;
|
|
129
|
+
}): PersistedHookMetadata {
|
|
130
|
+
const hasGrantField =
|
|
131
|
+
input.capabilities !== undefined ||
|
|
132
|
+
input.networkDestinations !== undefined ||
|
|
133
|
+
input.filesystemRoots !== undefined;
|
|
134
|
+
const hasHash = input.capabilityHash !== undefined;
|
|
135
|
+
|
|
136
|
+
if (!hasGrantField && !hasHash) {
|
|
137
|
+
if (input.functionHook !== undefined && typeof input.functionHook !== "boolean")
|
|
138
|
+
invalidHookMetadata(input.plugin, "functionHook must be boolean");
|
|
139
|
+
if (input.functionHook === true)
|
|
140
|
+
invalidHookMetadata(
|
|
141
|
+
input.plugin,
|
|
142
|
+
"functionHook requires capabilities, destinations, roots, and capabilityHash",
|
|
143
|
+
);
|
|
144
|
+
return { grant: compatibilityFunctionHookGrant(input.event), functionHook: false };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
input.functionHook !== true ||
|
|
149
|
+
!Array.isArray(input.capabilities) ||
|
|
150
|
+
!Array.isArray(input.networkDestinations) ||
|
|
151
|
+
!Array.isArray(input.filesystemRoots) ||
|
|
152
|
+
typeof input.capabilityHash !== "string"
|
|
153
|
+
)
|
|
154
|
+
invalidHookMetadata(input.plugin, "functionHook grants must include canonical arrays and capabilityHash");
|
|
155
|
+
|
|
156
|
+
let grant: FunctionHookGrant;
|
|
157
|
+
try {
|
|
158
|
+
grant = normalizeFunctionHookGrant({
|
|
159
|
+
capabilities: input.capabilities as FunctionHookGrant["capabilities"],
|
|
160
|
+
networkDestinations: input.networkDestinations as string[],
|
|
161
|
+
filesystemRoots: input.filesystemRoots as string[],
|
|
162
|
+
});
|
|
163
|
+
} catch {
|
|
164
|
+
invalidHookMetadata(input.plugin, "grant values are invalid");
|
|
165
|
+
}
|
|
166
|
+
if (
|
|
167
|
+
!sameValues(input.capabilities as unknown[], grant.capabilities) ||
|
|
168
|
+
!sameValues(input.networkDestinations as unknown[], grant.networkDestinations) ||
|
|
169
|
+
!sameValues(input.filesystemRoots as unknown[], grant.filesystemRoots)
|
|
170
|
+
)
|
|
171
|
+
invalidHookMetadata(input.plugin, "grant values are not normalized");
|
|
172
|
+
const expectedHash = functionHookGrantHash(grant);
|
|
173
|
+
if ((input.capabilityHash as string).toLowerCase() !== expectedHash)
|
|
174
|
+
invalidHookMetadata(input.plugin, "capabilityHash does not match the normalized grant");
|
|
175
|
+
return { grant, functionHook: true };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function assertCompiledHookMetadata(
|
|
179
|
+
declared: DeclaredHook,
|
|
180
|
+
expected: NormalizedHookSurface | undefined,
|
|
181
|
+
): PersistedHookMetadata {
|
|
182
|
+
const persisted = readPersistedHookMetadata(declared);
|
|
183
|
+
if (!expected) {
|
|
184
|
+
invalidHookMetadata(
|
|
185
|
+
declared.plugin,
|
|
186
|
+
`hook ${declared.extensionId ?? declared.relativePath} is not in the installed manifest`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const expectedFunctionHook = expected.functionHook === true;
|
|
190
|
+
if (
|
|
191
|
+
declared.extensionId !== expected.extensionId ||
|
|
192
|
+
declared.event !== expected.event ||
|
|
193
|
+
declared.target !== expected.target ||
|
|
194
|
+
declared.phase !== expected.phase ||
|
|
195
|
+
declared.relativePath !== expected.relativePath ||
|
|
196
|
+
declared.implementationHash !== expected.implementationHash
|
|
197
|
+
)
|
|
198
|
+
invalidHookMetadata(declared.plugin, "identity or implementation metadata does not match the installed manifest");
|
|
199
|
+
if (persisted.functionHook !== expectedFunctionHook)
|
|
200
|
+
invalidHookMetadata(declared.plugin, "functionHook does not match the installed manifest");
|
|
201
|
+
if (!expectedFunctionHook) return persisted;
|
|
202
|
+
if (
|
|
203
|
+
!sameValues(persisted.grant.capabilities, expected.capabilities ?? []) ||
|
|
204
|
+
!sameValues(persisted.grant.networkDestinations, expected.networkDestinations ?? []) ||
|
|
205
|
+
!sameValues(persisted.grant.filesystemRoots, expected.filesystemRoots ?? []) ||
|
|
206
|
+
typeof expected.capabilityHash !== "string" ||
|
|
207
|
+
functionHookGrantHash(persisted.grant).toLowerCase() !== expected.capabilityHash.toLowerCase()
|
|
208
|
+
)
|
|
209
|
+
invalidHookMetadata(declared.plugin, "grant metadata does not match the installed manifest");
|
|
210
|
+
return persisted;
|
|
60
211
|
}
|
|
61
212
|
|
|
62
213
|
async function collectDeclaredHooks(
|
|
63
214
|
entries: readonly GjcPluginRegistryEntry[],
|
|
64
215
|
invalidHookIds = new Set<string>(),
|
|
216
|
+
activationGeneration?: number,
|
|
65
217
|
): Promise<DeclaredHook[]> {
|
|
66
218
|
const out: DeclaredHook[] = [];
|
|
67
219
|
for (const entry of entries) {
|
|
@@ -70,16 +222,23 @@ async function collectDeclaredHooks(
|
|
|
70
222
|
for (const h of entry.surfaces.hooks) {
|
|
71
223
|
if (disabled.has(h.extensionId) || invalidHookIds.has(`${entry.scope}:${entry.name}:${h.extensionId}`))
|
|
72
224
|
continue;
|
|
73
|
-
const implementationPath = await resolveConstrainedHookFile(entry.pluginRoot, h.relativePath);
|
|
74
225
|
out.push({
|
|
75
226
|
plugin: entry.name,
|
|
76
227
|
scope: entry.scope,
|
|
228
|
+
extensionId: h.extensionId,
|
|
77
229
|
event: h.event,
|
|
78
230
|
target: h.target,
|
|
79
231
|
phase: h.phase,
|
|
80
|
-
relativePath:
|
|
232
|
+
relativePath: h.relativePath,
|
|
81
233
|
implementationHash:
|
|
82
234
|
"implementationHash" in h && typeof h.implementationHash === "string" ? h.implementationHash : undefined,
|
|
235
|
+
capabilities: h.capabilities,
|
|
236
|
+
networkDestinations: h.networkDestinations,
|
|
237
|
+
filesystemRoots: h.filesystemRoots,
|
|
238
|
+
capabilityHash: h.capabilityHash,
|
|
239
|
+
pluginRoot: entry.pluginRoot,
|
|
240
|
+
functionHook: h.functionHook,
|
|
241
|
+
activationGeneration,
|
|
83
242
|
});
|
|
84
243
|
}
|
|
85
244
|
}
|
|
@@ -89,24 +248,68 @@ async function collectDeclaredHooks(
|
|
|
89
248
|
/** Lazy declaration for one constrained hook. Importing this descriptor is metadata-only. */
|
|
90
249
|
export class ConstrainedPluginHookDescriptor {
|
|
91
250
|
readonly plugin: string;
|
|
92
|
-
readonly scope
|
|
251
|
+
readonly scope?: GjcPluginScope;
|
|
252
|
+
readonly extensionId?: string;
|
|
93
253
|
readonly event: string;
|
|
94
254
|
readonly target?: string;
|
|
95
255
|
readonly phase?: "before" | "after";
|
|
96
256
|
readonly relativePath: string;
|
|
97
257
|
readonly implementationHash?: string;
|
|
258
|
+
readonly capabilities?: string[];
|
|
259
|
+
readonly networkDestinations?: string[];
|
|
260
|
+
readonly filesystemRoots?: string[];
|
|
261
|
+
readonly capabilityHash?: string;
|
|
262
|
+
readonly pluginRoot?: string;
|
|
263
|
+
readonly grant: FunctionHookGrant;
|
|
264
|
+
readonly functionHook: boolean;
|
|
265
|
+
readonly activationGeneration?: number;
|
|
266
|
+
readonly persistedFunctionHook?: unknown;
|
|
98
267
|
|
|
99
268
|
constructor(input: DeclaredHook) {
|
|
100
269
|
this.plugin = input.plugin;
|
|
101
270
|
this.scope = input.scope;
|
|
271
|
+
this.extensionId = input.extensionId;
|
|
102
272
|
this.event = input.event;
|
|
103
273
|
this.target = input.target;
|
|
104
274
|
this.phase = input.phase;
|
|
105
275
|
this.relativePath = input.relativePath;
|
|
106
276
|
this.implementationHash = input.implementationHash;
|
|
277
|
+
this.pluginRoot = input.pluginRoot;
|
|
278
|
+
this.functionHook = input.functionHook === true;
|
|
279
|
+
this.persistedFunctionHook = input.functionHook;
|
|
280
|
+
this.capabilities = input.capabilities;
|
|
281
|
+
this.networkDestinations = input.networkDestinations;
|
|
282
|
+
this.filesystemRoots = input.filesystemRoots;
|
|
283
|
+
this.capabilityHash = input.capabilityHash;
|
|
284
|
+
this.activationGeneration = input.activationGeneration;
|
|
285
|
+
try {
|
|
286
|
+
this.grant =
|
|
287
|
+
input.capabilities === undefined &&
|
|
288
|
+
input.networkDestinations === undefined &&
|
|
289
|
+
input.filesystemRoots === undefined
|
|
290
|
+
? compatibilityFunctionHookGrant(input.event)
|
|
291
|
+
: normalizeFunctionHookGrant({
|
|
292
|
+
capabilities: input.capabilities as FunctionHookGrant["capabilities"] | undefined,
|
|
293
|
+
networkDestinations: input.networkDestinations,
|
|
294
|
+
filesystemRoots: input.filesystemRoots,
|
|
295
|
+
});
|
|
296
|
+
} catch {
|
|
297
|
+
// Keep construction metadata-only; load() performs the fail-closed
|
|
298
|
+
// validation and reports a quarantine-safe error.
|
|
299
|
+
this.grant = compatibilityFunctionHookGrant(input.event);
|
|
300
|
+
}
|
|
107
301
|
}
|
|
108
302
|
|
|
109
303
|
async load(): Promise<ConstrainedPluginHook> {
|
|
304
|
+
const metadata = readPersistedHookMetadata({
|
|
305
|
+
plugin: this.plugin,
|
|
306
|
+
event: this.event,
|
|
307
|
+
capabilities: this.capabilities,
|
|
308
|
+
networkDestinations: this.networkDestinations,
|
|
309
|
+
filesystemRoots: this.filesystemRoots,
|
|
310
|
+
capabilityHash: this.capabilityHash,
|
|
311
|
+
functionHook: this.persistedFunctionHook,
|
|
312
|
+
});
|
|
110
313
|
const normalized = normalizePluginHook({
|
|
111
314
|
declaredEvent: this.event,
|
|
112
315
|
target: this.target,
|
|
@@ -120,8 +323,21 @@ export class ConstrainedPluginHookDescriptor {
|
|
|
120
323
|
normalized.diagnostics.map(diagnostic => `${diagnostic.code}: ${diagnostic.message}`).join("; "),
|
|
121
324
|
);
|
|
122
325
|
}
|
|
123
|
-
if (this.
|
|
124
|
-
|
|
326
|
+
if (!this.pluginRoot) {
|
|
327
|
+
throw new GjcPluginLoadError(
|
|
328
|
+
"security_policy",
|
|
329
|
+
`GJC plugin hook "${this.plugin}" has no installed root; refusing to import an unconfined path`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (metadata.functionHook) {
|
|
333
|
+
throw new GjcPluginLoadError(
|
|
334
|
+
"security_policy",
|
|
335
|
+
`Plugin function hook "${this.plugin}" requires an isolated runtime and cannot execute in the host realm`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const resolvedPath = await resolveConstrainedHookFile(this.pluginRoot, this.relativePath);
|
|
339
|
+
if (this.implementationHash) await verifyImplementationHash(resolvedPath, this.implementationHash);
|
|
340
|
+
const registered: { event: string; handler: (...a: never[]) => unknown }[] = [];
|
|
125
341
|
const deny = (method: string) => () => {
|
|
126
342
|
throw new GjcPluginLoadError(
|
|
127
343
|
"security_policy",
|
|
@@ -129,16 +345,21 @@ export class ConstrainedPluginHookDescriptor {
|
|
|
129
345
|
);
|
|
130
346
|
};
|
|
131
347
|
const constrainedApi: Record<string, unknown> = {
|
|
132
|
-
on: (event: string, handler: (...a:
|
|
348
|
+
on: (event: string, handler: (...a: never[]) => unknown) => registered.push({ event, handler }),
|
|
133
349
|
logger,
|
|
134
350
|
};
|
|
135
351
|
for (const method of DENIED_API_METHODS) constrainedApi[method] = deny(method);
|
|
136
|
-
const mod = await import(
|
|
352
|
+
const mod = await import(resolvedPath);
|
|
137
353
|
const factory = mod.default ?? mod;
|
|
138
354
|
if (typeof factory !== "function")
|
|
139
355
|
throw new GjcPluginLoadError("invalid_hook", "Plugin hook must export a default function");
|
|
140
356
|
await (factory as (api: unknown) => unknown)(constrainedApi);
|
|
141
|
-
if (
|
|
357
|
+
if (this.implementationHash) await verifyImplementationHash(resolvedPath, this.implementationHash);
|
|
358
|
+
if (
|
|
359
|
+
registered.length !== 1 ||
|
|
360
|
+
registered[0]?.event !== this.event ||
|
|
361
|
+
typeof registered[0]?.handler !== "function"
|
|
362
|
+
) {
|
|
142
363
|
throw new GjcPluginLoadError(
|
|
143
364
|
"runtime_mismatch",
|
|
144
365
|
`Plugin hook registered ${JSON.stringify(registered.map(r => r.event))}, expected exactly ["${this.event}"]`,
|
|
@@ -146,10 +367,28 @@ export class ConstrainedPluginHookDescriptor {
|
|
|
146
367
|
}
|
|
147
368
|
return {
|
|
148
369
|
plugin: this.plugin,
|
|
370
|
+
scope: this.scope,
|
|
371
|
+
extensionId: this.extensionId,
|
|
149
372
|
event: this.event,
|
|
150
373
|
target: this.target,
|
|
151
374
|
phase: this.phase,
|
|
152
375
|
handler: registered[0].handler,
|
|
376
|
+
grant: metadata.grant,
|
|
377
|
+
functionHook: metadata.functionHook,
|
|
378
|
+
...(this.scope && this.pluginRoot && this.extensionId
|
|
379
|
+
? {
|
|
380
|
+
provenance: {
|
|
381
|
+
source: "plugin-bundle" as const,
|
|
382
|
+
scope: this.scope,
|
|
383
|
+
plugin: this.plugin,
|
|
384
|
+
path: this.relativePath,
|
|
385
|
+
extensionId: this.extensionId,
|
|
386
|
+
...(this.activationGeneration === undefined
|
|
387
|
+
? {}
|
|
388
|
+
: { activationGeneration: this.activationGeneration }),
|
|
389
|
+
},
|
|
390
|
+
}
|
|
391
|
+
: {}),
|
|
153
392
|
};
|
|
154
393
|
}
|
|
155
394
|
}
|
|
@@ -165,7 +404,9 @@ async function loadOneHook(
|
|
|
165
404
|
quarantine: {
|
|
166
405
|
identity: bundleIdentity(declared.scope, declared.plugin),
|
|
167
406
|
plugin: declared.plugin,
|
|
168
|
-
surfaceId:
|
|
407
|
+
surfaceId:
|
|
408
|
+
declared.extensionId ??
|
|
409
|
+
`hook:${declared.event}:${declared.phase ?? ""}:${declared.target ?? "*"}:${path.basename(declared.relativePath)}`,
|
|
169
410
|
code,
|
|
170
411
|
message: error instanceof Error ? error.message : String(error),
|
|
171
412
|
},
|
|
@@ -178,13 +419,32 @@ async function loadOneHook(
|
|
|
178
419
|
* `cwd`, applying hash-drift + collision quarantine first. Returns empty when
|
|
179
420
|
* no plugins are installed.
|
|
180
421
|
*/
|
|
181
|
-
export async function loadConstrainedPluginHooks(input: {
|
|
422
|
+
export async function loadConstrainedPluginHooks(input: {
|
|
423
|
+
cwd: string;
|
|
424
|
+
activationGeneration?: number;
|
|
425
|
+
}): Promise<ConstrainedHookLoadResult> {
|
|
182
426
|
const effective = await loadEffectiveGjcPluginRegistry(input.cwd);
|
|
183
427
|
if (effective.length === 0) return { hooks: [], quarantine: [] };
|
|
184
428
|
const preQuarantine: SessionQuarantine[] = [];
|
|
185
429
|
const invalidHookIds = new Set<string>();
|
|
186
430
|
for (const entry of effective) {
|
|
187
431
|
if (!entry.enabled) continue;
|
|
432
|
+
let compiledHooks = new Map<string, NormalizedHookSurface>();
|
|
433
|
+
if (entry.surfaces.hooks.length > 0) {
|
|
434
|
+
try {
|
|
435
|
+
const compiled = await compileGjcPluginBundle(entry.pluginRoot);
|
|
436
|
+
compiledHooks = new Map(compiled.surfaces.hooks.map(hook => [hook.extensionId, hook]));
|
|
437
|
+
} catch (error) {
|
|
438
|
+
preQuarantine.push({
|
|
439
|
+
identity: bundleIdentity(entry.scope, entry.name),
|
|
440
|
+
plugin: entry.name,
|
|
441
|
+
surfaceId: `plugin:${entry.name}`,
|
|
442
|
+
code: error instanceof GjcPluginLoadError ? error.code : "invalid_hook",
|
|
443
|
+
message: error instanceof Error ? error.message : String(error),
|
|
444
|
+
});
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
188
448
|
for (const hook of entry.surfaces.hooks) {
|
|
189
449
|
if (entry.disabledSurfaceIds.includes(hook.extensionId)) continue;
|
|
190
450
|
const normalized = normalizePluginHook({
|
|
@@ -194,20 +454,58 @@ export async function loadConstrainedPluginHooks(input: { cwd: string }): Promis
|
|
|
194
454
|
plugin: entry.name,
|
|
195
455
|
source: hook.relativePath,
|
|
196
456
|
});
|
|
197
|
-
if (normalized.hook)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
457
|
+
if (!normalized.hook) {
|
|
458
|
+
invalidHookIds.add(`${entry.scope}:${entry.name}:${hook.extensionId}`);
|
|
459
|
+
preQuarantine.push({
|
|
460
|
+
identity: bundleIdentity(entry.scope, entry.name),
|
|
461
|
+
plugin: entry.name,
|
|
462
|
+
surfaceId: hook.extensionId,
|
|
463
|
+
code: "invalid_hook",
|
|
464
|
+
message: normalized.diagnostics
|
|
465
|
+
.map(diagnostic => `${diagnostic.code}: ${diagnostic.message}`)
|
|
466
|
+
.join("; "),
|
|
467
|
+
});
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
assertCompiledHookMetadata(
|
|
472
|
+
{
|
|
473
|
+
plugin: entry.name,
|
|
474
|
+
scope: entry.scope,
|
|
475
|
+
extensionId: hook.extensionId,
|
|
476
|
+
event: hook.event,
|
|
477
|
+
target: hook.target,
|
|
478
|
+
phase: hook.phase,
|
|
479
|
+
relativePath: hook.relativePath,
|
|
480
|
+
implementationHash: hook.implementationHash,
|
|
481
|
+
capabilities: hook.capabilities,
|
|
482
|
+
networkDestinations: hook.networkDestinations,
|
|
483
|
+
filesystemRoots: hook.filesystemRoots,
|
|
484
|
+
capabilityHash: hook.capabilityHash,
|
|
485
|
+
pluginRoot: entry.pluginRoot,
|
|
486
|
+
functionHook: hook.functionHook,
|
|
487
|
+
},
|
|
488
|
+
compiledHooks.get(hook.extensionId),
|
|
489
|
+
);
|
|
490
|
+
} catch (error) {
|
|
491
|
+
invalidHookIds.add(`${entry.scope}:${entry.name}:${hook.extensionId}`);
|
|
492
|
+
preQuarantine.push({
|
|
493
|
+
identity: bundleIdentity(entry.scope, entry.name),
|
|
494
|
+
plugin: entry.name,
|
|
495
|
+
surfaceId: hook.extensionId,
|
|
496
|
+
code: error instanceof GjcPluginLoadError ? error.code : "invalid_hook",
|
|
497
|
+
message: error instanceof Error ? error.message : String(error),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
206
500
|
}
|
|
207
501
|
const drift = await verifyEntryHashes(entry);
|
|
208
502
|
if (drift) preQuarantine.push(drift);
|
|
209
503
|
for (const hook of entry.surfaces.hooks) {
|
|
210
|
-
if (
|
|
504
|
+
if (
|
|
505
|
+
entry.disabledSurfaceIds.includes(hook.extensionId) ||
|
|
506
|
+
invalidHookIds.has(`${entry.scope}:${entry.name}:${hook.extensionId}`)
|
|
507
|
+
)
|
|
508
|
+
continue;
|
|
211
509
|
try {
|
|
212
510
|
await resolveConstrainedHookFile(entry.pluginRoot, hook.relativePath);
|
|
213
511
|
} catch (error) {
|
|
@@ -223,7 +521,7 @@ export async function loadConstrainedPluginHooks(input: { cwd: string }): Promis
|
|
|
223
521
|
}
|
|
224
522
|
}
|
|
225
523
|
const { active, quarantine } = validateSessionBundles(effective, {}, preQuarantine);
|
|
226
|
-
const declared = await collectDeclaredHooks(active, invalidHookIds);
|
|
524
|
+
const declared = await collectDeclaredHooks(active, invalidHookIds, input.activationGeneration);
|
|
227
525
|
const hooks: ConstrainedPluginHook[] = [];
|
|
228
526
|
for (const d of declared) {
|
|
229
527
|
const { hook, quarantine: q } = await loadOneHook(d);
|
|
@@ -27,6 +27,117 @@ function isEnoent(error: unknown): boolean {
|
|
|
27
27
|
return (error as NodeJS.ErrnoException)?.code === "ENOENT";
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function registryRecord(value: unknown, field: string): Record<string, unknown> {
|
|
31
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
32
|
+
throw new GjcPluginLoadError("invalid_manifest", `Invalid GJC plugin registry field ${field}`);
|
|
33
|
+
}
|
|
34
|
+
return value as Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function rejectRegistryKeys(value: Record<string, unknown>, allowed: readonly string[], field: string): void {
|
|
38
|
+
const allowedSet = new Set(allowed);
|
|
39
|
+
for (const key of Object.keys(value)) {
|
|
40
|
+
if (!allowedSet.has(key)) {
|
|
41
|
+
throw new GjcPluginLoadError("invalid_manifest", `Unknown GJC plugin registry field ${field}.${key}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateRegistryEntryShape(value: unknown, field: string): void {
|
|
47
|
+
const entry = registryRecord(value, field);
|
|
48
|
+
rejectRegistryKeys(
|
|
49
|
+
entry,
|
|
50
|
+
[
|
|
51
|
+
"name",
|
|
52
|
+
"version",
|
|
53
|
+
"scope",
|
|
54
|
+
"enabled",
|
|
55
|
+
"pluginRoot",
|
|
56
|
+
"manifestPath",
|
|
57
|
+
"manifestHash",
|
|
58
|
+
"source",
|
|
59
|
+
"installedAt",
|
|
60
|
+
"updatedAt",
|
|
61
|
+
"copiedFiles",
|
|
62
|
+
"surfaces",
|
|
63
|
+
"disabledSurfaceIds",
|
|
64
|
+
"quarantine",
|
|
65
|
+
"migration",
|
|
66
|
+
],
|
|
67
|
+
field,
|
|
68
|
+
);
|
|
69
|
+
const source = registryRecord(entry.source, `${field}.source`);
|
|
70
|
+
rejectRegistryKeys(source, ["kind", "uri", "ref", "sha", "resolvedAt"], `${field}.source`);
|
|
71
|
+
const surfaces = registryRecord(entry.surfaces, `${field}.surfaces`);
|
|
72
|
+
rejectRegistryKeys(
|
|
73
|
+
surfaces,
|
|
74
|
+
["subskills", "tools", "hooks", "mcps", "systemAppendices", "agentAppendices"],
|
|
75
|
+
`${field}.surfaces`,
|
|
76
|
+
);
|
|
77
|
+
const surfaceKeys: Record<string, readonly string[]> = {
|
|
78
|
+
subskills: [
|
|
79
|
+
"extensionId",
|
|
80
|
+
"name",
|
|
81
|
+
"description",
|
|
82
|
+
"parent",
|
|
83
|
+
"phase",
|
|
84
|
+
"activationArg",
|
|
85
|
+
"relativePath",
|
|
86
|
+
"sha256",
|
|
87
|
+
"toolRefs",
|
|
88
|
+
],
|
|
89
|
+
tools: [
|
|
90
|
+
"extensionId",
|
|
91
|
+
"name",
|
|
92
|
+
"relativePath",
|
|
93
|
+
"sha256",
|
|
94
|
+
"description",
|
|
95
|
+
"schema",
|
|
96
|
+
"schemaHash",
|
|
97
|
+
"implementationHash",
|
|
98
|
+
"presentationHash",
|
|
99
|
+
"metadataVersion",
|
|
100
|
+
],
|
|
101
|
+
hooks: [
|
|
102
|
+
"extensionId",
|
|
103
|
+
"name",
|
|
104
|
+
"event",
|
|
105
|
+
"target",
|
|
106
|
+
"phase",
|
|
107
|
+
"relativePath",
|
|
108
|
+
"sha256",
|
|
109
|
+
"implementationHash",
|
|
110
|
+
"capabilities",
|
|
111
|
+
"networkDestinations",
|
|
112
|
+
"filesystemRoots",
|
|
113
|
+
"capabilityHash",
|
|
114
|
+
"functionHook",
|
|
115
|
+
],
|
|
116
|
+
mcps: ["extensionId", "name", "transport", "configHash", "config"],
|
|
117
|
+
systemAppendices: ["extensionId", "name", "relativePath", "content", "contentHash", "bytes"],
|
|
118
|
+
agentAppendices: ["extensionId", "name", "relativePath", "content", "contentHash", "bytes", "agent"],
|
|
119
|
+
};
|
|
120
|
+
for (const [surfaceName, allowed] of Object.entries(surfaceKeys)) {
|
|
121
|
+
const items = surfaces[surfaceName];
|
|
122
|
+
if (!Array.isArray(items))
|
|
123
|
+
throw new GjcPluginLoadError("invalid_manifest", `Invalid ${field}.surfaces.${surfaceName}`);
|
|
124
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
125
|
+
const item = registryRecord(items[index], `${field}.surfaces.${surfaceName}[${index}]`);
|
|
126
|
+
rejectRegistryKeys(item, allowed, `${field}.surfaces.${surfaceName}[${index}]`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(entry.copiedFiles) || !Array.isArray(entry.disabledSurfaceIds)) {
|
|
130
|
+
throw new GjcPluginLoadError("invalid_manifest", `Invalid GJC plugin registry arrays at ${field}`);
|
|
131
|
+
}
|
|
132
|
+
if (entry.quarantine !== undefined && !Array.isArray(entry.quarantine)) {
|
|
133
|
+
throw new GjcPluginLoadError("invalid_manifest", `Invalid ${field}.quarantine`);
|
|
134
|
+
}
|
|
135
|
+
if (entry.migration !== undefined) {
|
|
136
|
+
const migration = registryRecord(entry.migration, `${field}.migration`);
|
|
137
|
+
rejectRegistryKeys(migration, ["status", "metadataVersion", "migratedAt", "failure"], `${field}.migration`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
30
141
|
/**
|
|
31
142
|
* Deterministic ordering: scope (user before project) -> normalized name ->
|
|
32
143
|
* resolved plugin root. Collisions are errors elsewhere; order only controls
|
|
@@ -69,8 +180,9 @@ async function readRegistryRaw(scope: GjcPluginScope, cwd: string): Promise<GjcP
|
|
|
69
180
|
);
|
|
70
181
|
if (
|
|
71
182
|
!Array.isArray(registry.plugins) ||
|
|
72
|
-
registry.plugins.some(plugin => {
|
|
183
|
+
registry.plugins.some((plugin, index) => {
|
|
73
184
|
if (!plugin || typeof plugin !== "object") return true;
|
|
185
|
+
validateRegistryEntryShape(plugin, `plugins[${index}]`);
|
|
74
186
|
const entry = plugin as GjcPluginRegistryEntry;
|
|
75
187
|
return (
|
|
76
188
|
entry.scope !== scope ||
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { redactCrashSecrets } from "@gajae-code/utils";
|
|
1
2
|
import { identityEquals, identityKey } from "./lifecycle-reconciliation";
|
|
2
3
|
import type { GjcBundleIdentity, GjcRuntimeFinding, GjcRuntimeSnapshot, GjcRuntimeSnapshotState } from "./types";
|
|
3
4
|
|
|
@@ -28,7 +29,11 @@ export class GjcRuntimeFindingAccumulator {
|
|
|
28
29
|
constructor(readonly generation: number) {}
|
|
29
30
|
|
|
30
31
|
add(finding: GjcRuntimeFinding): void {
|
|
31
|
-
this.findings.push(
|
|
32
|
+
this.findings.push({
|
|
33
|
+
...finding,
|
|
34
|
+
message: redactCrashSecrets(finding.message.replace(/[\u0000-\u001f\u007f-\u009f]/gu, "?")).slice(0, 2048),
|
|
35
|
+
generation: this.generation,
|
|
36
|
+
});
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
addAll(findings: readonly GjcRuntimeFinding[]): void {
|
|
@@ -40,7 +45,20 @@ export class GjcRuntimeFindingAccumulator {
|
|
|
40
45
|
const seen = new Set<string>();
|
|
41
46
|
const unique: GjcRuntimeFinding[] = [];
|
|
42
47
|
for (const finding of this.findings) {
|
|
43
|
-
const key = [
|
|
48
|
+
const key = [
|
|
49
|
+
identityKey(finding.identity),
|
|
50
|
+
finding.surfaceId,
|
|
51
|
+
finding.code,
|
|
52
|
+
finding.message,
|
|
53
|
+
finding.decision ?? "",
|
|
54
|
+
finding.event ?? "",
|
|
55
|
+
finding.phase ?? "",
|
|
56
|
+
JSON.stringify(finding.requestedCapabilities ?? []),
|
|
57
|
+
JSON.stringify(finding.effectiveCapabilities ?? []),
|
|
58
|
+
finding.capabilityHash ?? "",
|
|
59
|
+
JSON.stringify(finding.provenance ?? {}),
|
|
60
|
+
JSON.stringify(finding.audit ?? {}),
|
|
61
|
+
].join("\u0000");
|
|
44
62
|
if (seen.has(key)) continue;
|
|
45
63
|
seen.add(key);
|
|
46
64
|
unique.push(finding);
|