@mcrescenzo/opencode-workflows 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +134 -0
- package/CONTRIBUTING.md +95 -0
- package/LICENSE +21 -0
- package/README.md +746 -0
- package/SECURITY.md +38 -0
- package/commands/repo-bughunt.md +94 -0
- package/commands/repo-review.md +148 -0
- package/commands/workflow-live-gates-release-check.md +143 -0
- package/docs/workflow-plugin.md +400 -0
- package/opencode-workflows.js +5 -0
- package/package.json +86 -0
- package/skills/opencode-workflow-authoring/SKILL.md +180 -0
- package/skills/repo-review-command-protocol/SKILL.md +56 -0
- package/skills/workflow-model-tiering/SKILL.md +57 -0
- package/skills/workflow-plan-review/SKILL.md +91 -0
- package/workflow-kernel/approval-hashing.js +39 -0
- package/workflow-kernel/async-util.js +33 -0
- package/workflow-kernel/audited-shell-policy.js +200 -0
- package/workflow-kernel/authority-policy.js +670 -0
- package/workflow-kernel/budget-accounting.js +142 -0
- package/workflow-kernel/capability-adapter.js +753 -0
- package/workflow-kernel/child-agent-runner.js +1264 -0
- package/workflow-kernel/constants.js +117 -0
- package/workflow-kernel/diagnostics.js +152 -0
- package/workflow-kernel/drain-runtime.js +421 -0
- package/workflow-kernel/errors.js +181 -0
- package/workflow-kernel/event-journal.js +487 -0
- package/workflow-kernel/extension-registry.js +144 -0
- package/workflow-kernel/free-text-redactor.js +91 -0
- package/workflow-kernel/gate-shapes.js +82 -0
- package/workflow-kernel/git-util.js +45 -0
- package/workflow-kernel/index.js +72 -0
- package/workflow-kernel/integration-mode.js +155 -0
- package/workflow-kernel/lane-effort-policy.js +134 -0
- package/workflow-kernel/lifecycle-control.js +608 -0
- package/workflow-kernel/live-gate-probes.js +916 -0
- package/workflow-kernel/notification-toast-cards.js +393 -0
- package/workflow-kernel/notification-toast-policy.js +179 -0
- package/workflow-kernel/notification-toast-scope.js +100 -0
- package/workflow-kernel/notification-toast.js +287 -0
- package/workflow-kernel/path-policy.js +219 -0
- package/workflow-kernel/result-readback.js +106 -0
- package/workflow-kernel/role-template-loading.js +606 -0
- package/workflow-kernel/run-context.js +139 -0
- package/workflow-kernel/run-observability.js +43 -0
- package/workflow-kernel/run-store-fs.js +231 -0
- package/workflow-kernel/run-store-locks.js +180 -0
- package/workflow-kernel/run-store-projections.js +421 -0
- package/workflow-kernel/run-store-rehydrate.js +68 -0
- package/workflow-kernel/run-store-state.js +147 -0
- package/workflow-kernel/run-store-status-format.js +1154 -0
- package/workflow-kernel/run-store-status.js +107 -0
- package/workflow-kernel/sandbox-executor.js +1131 -0
- package/workflow-kernel/session-access.js +56 -0
- package/workflow-kernel/structured-output.js +143 -0
- package/workflow-kernel/test-fix-drain-adapter.js +119 -0
- package/workflow-kernel/text-json.js +136 -0
- package/workflow-kernel/workflow-plugin.js +3017 -0
- package/workflow-kernel/workflow-source.js +444 -0
- package/workflow-kernel/worktree-adapter.js +256 -0
- package/workflow-kernel/worktree-git.js +147 -0
- package/workflows/repo-bughunt.js +362 -0
- package/workflows/repo-cleanup.js +383 -0
- package/workflows/repo-complexity.js +404 -0
- package/workflows/repo-deps.js +457 -0
- package/workflows/repo-modernize.js +395 -0
- package/workflows/repo-perf.js +394 -0
- package/workflows/repo-review.js +831 -0
- package/workflows/repo-security-audit.js +466 -0
- package/workflows/repo-test-gaps.js +377 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { hasFunction } from "./text-json.js";
|
|
7
|
+
import { WorkflowAuthorityError } from "./errors.js";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_CONCURRENCY,
|
|
10
|
+
DEFAULT_CONCURRENCY_PROBE_LIMIT,
|
|
11
|
+
HARD_CONCURRENCY_LIMIT,
|
|
12
|
+
normalizeHardConcurrencyLimit,
|
|
13
|
+
} from "./constants.js";
|
|
14
|
+
import {
|
|
15
|
+
AD_HOC_AUTHORITY_PROFILE,
|
|
16
|
+
assertWriteWorkflowAllowed,
|
|
17
|
+
normalizeRequiredGates,
|
|
18
|
+
} from "./authority-policy.js";
|
|
19
|
+
import { sessionApi } from "./session-access.js";
|
|
20
|
+
import { hasWorkflowToast } from "./notification-toast.js";
|
|
21
|
+
// Gate-shape constructors live in gate-shapes.js; re-exported below so the public
|
|
22
|
+
// CapabilityAdapter surface (and the kernel barrel) is unchanged after the split.
|
|
23
|
+
import {
|
|
24
|
+
forcedGate,
|
|
25
|
+
gateAvailableUnverified,
|
|
26
|
+
gateBlocked,
|
|
27
|
+
shapeGate,
|
|
28
|
+
} from "./gate-shapes.js";
|
|
29
|
+
// Live-gate probe functions live in live-gate-probes.js. liveGateReport /
|
|
30
|
+
// promoteCapabilities below fan them out. `unwrapClientResult` is also used by
|
|
31
|
+
// createCapabilityAdapter's worktree create/remove paths, so it is imported here.
|
|
32
|
+
import {
|
|
33
|
+
probeBackgroundContinuationGate,
|
|
34
|
+
probeCancellationGate,
|
|
35
|
+
probeCommandScopedBash,
|
|
36
|
+
probeConcurrencyCapacityGate,
|
|
37
|
+
probeDeniedBash,
|
|
38
|
+
probeDirectoryRootingGate,
|
|
39
|
+
probeIntegrationWorktreeIsolationGate,
|
|
40
|
+
probeMcpAccessGate,
|
|
41
|
+
probeSecretReadDeny,
|
|
42
|
+
probeStructuredOutput,
|
|
43
|
+
probeStructuredOutputGate,
|
|
44
|
+
probeWorkflowNotificationGate,
|
|
45
|
+
probeWorktreeEditIsolationGate,
|
|
46
|
+
probeWorktreeGate,
|
|
47
|
+
unwrapClientResult,
|
|
48
|
+
} from "./live-gate-probes.js";
|
|
49
|
+
|
|
50
|
+
// R32 (opencode-workflows-6ti): a verified gate may carry a weak evidenceStrength.
|
|
51
|
+
// `in-process-smoke` exercises NOTHING of the target subsystem — it only yields the
|
|
52
|
+
// event loop (used by backgroundContinuation, which awaits a 0ms timeout and never
|
|
53
|
+
// touches the OpenCode background subsystem or proves restart survival). It must NOT
|
|
54
|
+
// satisfy a *required* authority gate on its own; an operator who adds such a gate to
|
|
55
|
+
// requiredGates must explicitly accept the weak strength via acceptWeakEvidence.
|
|
56
|
+
// Note: `no-attempt-fallback` is intentionally NOT down-ranked here — it is a
|
|
57
|
+
// compatibility fallback that still observes the real session API (retained deny rules)
|
|
58
|
+
// and the required permissionEnforcement gate depends on it passing.
|
|
59
|
+
// R31 (opencode-workflows-8w8): directoryRooting no longer produces a verified
|
|
60
|
+
// `model-text-only` strength at all — a model echoing the cwd in text is reported as
|
|
61
|
+
// available-unverified (verified=false), so it can never satisfy the required gate.
|
|
62
|
+
const NON_BEHAVIORAL_EVIDENCE_STRENGTHS = new Set([
|
|
63
|
+
"in-process-smoke",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// Module-level cache of in-flight / verified capability probe promises, keyed by
|
|
67
|
+
// redacted serverUrl + resolved directory. Each entry is `{ promise, verified, ts }`.
|
|
68
|
+
//
|
|
69
|
+
// R14: only *verified* probe results are retained for the process lifetime. A
|
|
70
|
+
// non-verified outcome (model answered from memory, transient server/transport
|
|
71
|
+
// error, timeout) is treated as ephemeral: the cache entry is dropped once it
|
|
72
|
+
// resolves so the next elevated workflow re-probes instead of inheriting a stale
|
|
73
|
+
// "available-unverified" / "blocked" that would otherwise lock out promotion until
|
|
74
|
+
// an OpenCode restart. We also coalesce concurrent in-flight probes (only one probe
|
|
75
|
+
// per key runs at a time) and apply a short TTL to verified entries so a long-lived
|
|
76
|
+
// process eventually re-confirms a previously-verified capability.
|
|
77
|
+
//
|
|
78
|
+
// The individual probe fields inside each entry self-expire via VERIFIED_PROBE_TTL_MS,
|
|
79
|
+
// but that only shrinks each entry's field set — it never removes the OUTER
|
|
80
|
+
// key -> cache binding. Without a cap on the outer map, a long-lived host that serves
|
|
81
|
+
// many projects/worktrees would accumulate one entry per distinct (serverUrl,
|
|
82
|
+
// directory) pair forever (AGENTS.md bounded-map invariant). So capabilityProbes is an
|
|
83
|
+
// LRU-bounded Map (analogous to BoundedTimestampSet in lifecycle-control.js): capped at
|
|
84
|
+
// CAPABILITY_PROBE_CACHE_MAX distinct keys with least-recently-used eviction on overflow.
|
|
85
|
+
const CAPABILITY_PROBE_CACHE_MAX = 200;
|
|
86
|
+
|
|
87
|
+
// LRU Map: consumed exactly like a plain Map (get/set/has/delete/clear/size/iteration),
|
|
88
|
+
// but capped at CAPABILITY_PROBE_CACHE_MAX distinct keys. Map preserves insertion order,
|
|
89
|
+
// so the first key is the least-recently-used; get() and set() re-insert their key to
|
|
90
|
+
// mark it most-recently-used, and set() evicts from the front once the cap is exceeded.
|
|
91
|
+
class BoundedProbeCache extends Map {
|
|
92
|
+
constructor(max = CAPABILITY_PROBE_CACHE_MAX) {
|
|
93
|
+
super();
|
|
94
|
+
this.max = max;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get(key) {
|
|
98
|
+
const value = super.get(key);
|
|
99
|
+
// Touch on access: re-inserting moves the key to the most-recently-used end so it
|
|
100
|
+
// is not the next eviction victim. Returns the same cache object, so callers that
|
|
101
|
+
// mutate cache[field] are unaffected.
|
|
102
|
+
if (value !== undefined && super.delete(key)) super.set(key, value);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
set(key, value) {
|
|
107
|
+
if (super.has(key)) super.delete(key);
|
|
108
|
+
super.set(key, value);
|
|
109
|
+
while (this.size > this.max) {
|
|
110
|
+
const oldest = super.keys().next().value;
|
|
111
|
+
if (oldest === undefined) break;
|
|
112
|
+
super.delete(oldest);
|
|
113
|
+
}
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const capabilityProbes = new BoundedProbeCache();
|
|
119
|
+
|
|
120
|
+
// Verified probe results are trusted for this long before a re-probe is allowed.
|
|
121
|
+
// Non-verified results are never cached past their in-flight resolution.
|
|
122
|
+
const VERIFIED_PROBE_TTL_MS = 10 * 60 * 1000;
|
|
123
|
+
|
|
124
|
+
const require = createRequire(import.meta.url);
|
|
125
|
+
|
|
126
|
+
// Resolve the package ENTRY (its "." export) then walk up to its package.json.
|
|
127
|
+
// We CANNOT require.resolve(`${packageName}/package.json`): @opencode-ai/plugin and
|
|
128
|
+
// @opencode-ai/sdk omit "./package.json" from their "exports" map, so that form throws
|
|
129
|
+
// ERR_PACKAGE_PATH_NOT_EXPORTED. Their "." export also declares ONLY an "import"
|
|
130
|
+
// condition (no "require"), so even require.resolve(packageName) throws
|
|
131
|
+
// ERR_PACKAGE_PATH_NOT_EXPORTED under CJS conditions — when it does, fall back to
|
|
132
|
+
// import.meta.resolve, which honors the "import" condition and yields the entry file.
|
|
133
|
+
// Resolution is anchored at THIS module's location (the plugin's own dependency tree),
|
|
134
|
+
// so it never depends on the monorepo root.
|
|
135
|
+
function resolvePackageEntry(packageName) {
|
|
136
|
+
try {
|
|
137
|
+
return require.resolve(packageName);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error?.code !== "ERR_PACKAGE_PATH_NOT_EXPORTED") return undefined;
|
|
140
|
+
try {
|
|
141
|
+
return fileURLToPath(import.meta.resolve(packageName));
|
|
142
|
+
} catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function readInstalledVersion(packageName) {
|
|
149
|
+
const entry = resolvePackageEntry(packageName);
|
|
150
|
+
if (!entry) return "unavailable";
|
|
151
|
+
let dir = path.dirname(entry);
|
|
152
|
+
for (let i = 0; i < 12; i++) {
|
|
153
|
+
const pj = path.join(dir, "package.json");
|
|
154
|
+
if (existsSync(pj)) {
|
|
155
|
+
try {
|
|
156
|
+
const parsed = JSON.parse(readFileSync(pj, "utf8"));
|
|
157
|
+
if (parsed.name === packageName) return parsed.version ?? "unavailable";
|
|
158
|
+
} catch { /* keep ascending */ }
|
|
159
|
+
}
|
|
160
|
+
const parent = path.dirname(dir);
|
|
161
|
+
if (parent === dir) break;
|
|
162
|
+
dir = parent;
|
|
163
|
+
}
|
|
164
|
+
return "unavailable";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function redactServerUrl(value) {
|
|
168
|
+
if (!value) return "unavailable";
|
|
169
|
+
try {
|
|
170
|
+
const parsed = new URL(String(value));
|
|
171
|
+
parsed.username = "";
|
|
172
|
+
parsed.password = "";
|
|
173
|
+
parsed.search = "";
|
|
174
|
+
parsed.hash = "";
|
|
175
|
+
return parsed.toString();
|
|
176
|
+
} catch {
|
|
177
|
+
return "present";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function createCapabilityAdapter(pluginContext) {
|
|
182
|
+
const forced = pluginContext.__workflowCapabilities ?? {};
|
|
183
|
+
const session = pluginContext.client?.session ?? {};
|
|
184
|
+
const worktreeClient = pluginContext.client?.worktree ?? pluginContext.client?.experimental?.worktree ?? {};
|
|
185
|
+
const diagnostics = {
|
|
186
|
+
opencodeVersion: forced.opencodeVersion ?? "not-probed",
|
|
187
|
+
pluginPackageVersion: readInstalledVersion("@opencode-ai/plugin"),
|
|
188
|
+
sdkPackageVersion: readInstalledVersion("@opencode-ai/sdk"),
|
|
189
|
+
serverUrl: redactServerUrl(pluginContext.serverUrl),
|
|
190
|
+
clientShape: {
|
|
191
|
+
sessionCreate: hasFunction(session, "create"),
|
|
192
|
+
sessionPrompt: hasFunction(session, "prompt"),
|
|
193
|
+
sessionAbort: hasFunction(session, "abort"),
|
|
194
|
+
sessionGet: hasFunction(session, "get"),
|
|
195
|
+
sessionUpdate: hasFunction(session, "update"),
|
|
196
|
+
sessionMessages: hasFunction(session, "messages"),
|
|
197
|
+
sessionShell: hasFunction(session, "shell"),
|
|
198
|
+
worktreeCreate: hasFunction(worktreeClient, "create"),
|
|
199
|
+
worktreeRemove: hasFunction(worktreeClient, "remove"),
|
|
200
|
+
worktreeReset: hasFunction(worktreeClient, "reset"),
|
|
201
|
+
worktreeList: hasFunction(worktreeClient, "list"),
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const childSession = forced.childSession ?? (diagnostics.clientShape.sessionCreate && diagnostics.clientShape.sessionPrompt ? "available" : "unavailable");
|
|
206
|
+
const permissions = forced.permissions ?? (diagnostics.clientShape.sessionCreate ? "available-unverified" : "unavailable");
|
|
207
|
+
const structuredOutput = forced.structuredOutput ?? (diagnostics.clientShape.sessionPrompt ? "available-unverified" : "unavailable");
|
|
208
|
+
const worktree = forced.worktree ?? (diagnostics.clientShape.worktreeCreate && diagnostics.clientShape.worktreeRemove ? "available-unverified" : "unavailable");
|
|
209
|
+
const directoryRooting = forced.directoryRooting ?? (diagnostics.clientShape.sessionCreate ? "available-unverified" : "unavailable");
|
|
210
|
+
const worktreeEditIsolation = forced.worktreeEditIsolation ?? (forced.worktree === "available" && forced.directoryRooting === "available" ? "available" : diagnostics.clientShape.worktreeCreate && diagnostics.clientShape.sessionCreate ? "available-unverified" : "unavailable");
|
|
211
|
+
const toast = forced.toast ?? (hasWorkflowToast(pluginContext) ? "available" : "unavailable");
|
|
212
|
+
|
|
213
|
+
let worktreeClientResolution;
|
|
214
|
+
async function resolveWorktreeClient() {
|
|
215
|
+
const injected = pluginContext.client?.worktree ?? pluginContext.client?.experimental?.worktree;
|
|
216
|
+
if (injected && hasFunction(injected, "create")) return { client: injected, kind: "injected" };
|
|
217
|
+
if (!worktreeClientResolution) {
|
|
218
|
+
worktreeClientResolution = (async () => {
|
|
219
|
+
// Production receives the v1 SDK client, which has no worktree resource. The
|
|
220
|
+
// worktree API lives on the v2 client, so build one lazily against the server URL.
|
|
221
|
+
try {
|
|
222
|
+
if (!pluginContext.serverUrl) return undefined;
|
|
223
|
+
const mod = await import("@opencode-ai/sdk/v2/client");
|
|
224
|
+
const createClient = mod.createOpencodeClient ?? mod.default?.createOpencodeClient;
|
|
225
|
+
if (typeof createClient !== "function") return undefined;
|
|
226
|
+
const v2 = createClient({ baseUrl: new URL(String(pluginContext.serverUrl)).origin });
|
|
227
|
+
return v2?.worktree && hasFunction(v2.worktree, "create") ? v2.worktree : undefined;
|
|
228
|
+
} catch {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
})();
|
|
232
|
+
}
|
|
233
|
+
const v2 = await worktreeClientResolution;
|
|
234
|
+
return v2 ? { client: v2, kind: "v2" } : undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
forced,
|
|
239
|
+
diagnostics,
|
|
240
|
+
capabilities: {
|
|
241
|
+
childSession,
|
|
242
|
+
permissions,
|
|
243
|
+
structuredOutput,
|
|
244
|
+
structuredOutputField: forced.structuredOutputField ?? "data.info.structured",
|
|
245
|
+
worktree,
|
|
246
|
+
directoryRooting,
|
|
247
|
+
worktreeEditIsolation,
|
|
248
|
+
backgroundContinuation: forced.backgroundContinuation ?? "available",
|
|
249
|
+
toast,
|
|
250
|
+
modelListing: forced.modelListing ?? "unavailable",
|
|
251
|
+
agentListing: forced.agentListing ?? "unavailable",
|
|
252
|
+
},
|
|
253
|
+
getStructured(result) {
|
|
254
|
+
const candidates = [
|
|
255
|
+
// `info.structured` is the field used by the installed @opencode-ai/sdk; keep it
|
|
256
|
+
// first. `info.structured_output` is accepted defensively in case a future SDK
|
|
257
|
+
// renames it, so schema lanes do not silently break on upgrade.
|
|
258
|
+
result?.data?.info?.structured,
|
|
259
|
+
result?.data?.info?.structured_output,
|
|
260
|
+
result?.data?.structured,
|
|
261
|
+
result?.data?.output,
|
|
262
|
+
result?.data?.result,
|
|
263
|
+
];
|
|
264
|
+
return candidates.find((candidate) => candidate !== undefined);
|
|
265
|
+
},
|
|
266
|
+
async hasWorktreeClient() {
|
|
267
|
+
return Boolean(await resolveWorktreeClient());
|
|
268
|
+
},
|
|
269
|
+
async createWorktree(input) {
|
|
270
|
+
if (typeof forced.createWorktree === "function") return await forced.createWorktree(input);
|
|
271
|
+
const resolved = await resolveWorktreeClient();
|
|
272
|
+
if (!resolved) throw new Error("Native Worktree API is unavailable");
|
|
273
|
+
// The real v2 client uses { directory, worktreeCreateInput }; an injected client
|
|
274
|
+
// (tests / legacy) uses the { body, query } shape. Shape the call per client kind.
|
|
275
|
+
const params = resolved.kind === "v2"
|
|
276
|
+
? { directory: input.directory, worktreeCreateInput: { name: input.name } }
|
|
277
|
+
: { body: { name: input.name, path: input.path, branch: input.branch }, query: { directory: input.directory } };
|
|
278
|
+
const result = unwrapClientResult(await resolved.client.create(params), "Worktree create");
|
|
279
|
+
return result?.data ?? result;
|
|
280
|
+
},
|
|
281
|
+
async removeWorktree(input) {
|
|
282
|
+
if (typeof forced.removeWorktree === "function") return await forced.removeWorktree(input);
|
|
283
|
+
const resolved = await resolveWorktreeClient();
|
|
284
|
+
if (!resolved || !hasFunction(resolved.client, "remove")) return undefined;
|
|
285
|
+
const params = resolved.kind === "v2"
|
|
286
|
+
? { worktreeRemoveInput: input?.directory ? { directory: input.directory } : undefined }
|
|
287
|
+
: { body: input?.body ?? (input?.id ? { id: input.id } : {}), query: input?.query };
|
|
288
|
+
const result = unwrapClientResult(await resolved.client.remove(params), "Worktree remove");
|
|
289
|
+
return result?.data ?? result;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Runs (or reuses) a single capability probe under `cache[field]`. `runProbe`
|
|
295
|
+
// produces the probe promise; `isVerified` inspects the resolved value to decide
|
|
296
|
+
// whether it earns a cache slot. Non-verified results — and any thrown probe — leave
|
|
297
|
+
// no cached entry, so a transient failure cannot permanently block later workflows.
|
|
298
|
+
async function cachedProbe(cache, field, runProbe, isVerified) {
|
|
299
|
+
const existing = cache[field];
|
|
300
|
+
if (existing) {
|
|
301
|
+
if (existing.verified) {
|
|
302
|
+
if (Date.now() - existing.ts < VERIFIED_PROBE_TTL_MS) return await existing.promise;
|
|
303
|
+
delete cache[field];
|
|
304
|
+
} else {
|
|
305
|
+
// A prior non-verified probe is still in flight; reuse it to coalesce rather
|
|
306
|
+
// than firing a duplicate. It self-evicts below once it resolves.
|
|
307
|
+
return await existing.promise;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const entry = { verified: false, ts: Date.now(), promise: undefined };
|
|
311
|
+
entry.promise = (async () => {
|
|
312
|
+
try {
|
|
313
|
+
const result = await runProbe();
|
|
314
|
+
if (isVerified(result)) {
|
|
315
|
+
entry.verified = true;
|
|
316
|
+
entry.ts = Date.now();
|
|
317
|
+
} else if (cache[field] === entry) {
|
|
318
|
+
delete cache[field];
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (cache[field] === entry) delete cache[field];
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
})();
|
|
326
|
+
cache[field] = entry;
|
|
327
|
+
return await entry.promise;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Drops cached capability probes so the next promotion / live-gate report re-probes
|
|
331
|
+
// from scratch. With no key, clears the whole module-level cache. Exposed through the
|
|
332
|
+
// workflow_live_gates tool (resetProbeCache) so an operator can recover after a
|
|
333
|
+
// transient probe failure without restarting OpenCode.
|
|
334
|
+
function invalidateCapabilityProbes(key) {
|
|
335
|
+
if (key === undefined) {
|
|
336
|
+
const count = capabilityProbes.size;
|
|
337
|
+
capabilityProbes.clear();
|
|
338
|
+
return count;
|
|
339
|
+
}
|
|
340
|
+
return capabilityProbes.delete(key) ? 1 : 0;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Promote shape-derived "available-unverified" capabilities to verified "available" only
|
|
344
|
+
// via behavioral probes. Skips any capability the caller forced (tests use forced
|
|
345
|
+
// capabilities as explicit evidence). Runs on the approved execution path before any lane,
|
|
346
|
+
// so run.capabilities is settled and lane signatures stay deterministic.
|
|
347
|
+
async function promoteCapabilities(pluginContext, toolContext, adapter, authority, options = {}) {
|
|
348
|
+
const forced = adapter.forced ?? {};
|
|
349
|
+
const key = `${redactServerUrl(pluginContext.serverUrl)}:${path.resolve(toolContext.worktree || toolContext.directory || ".")}`;
|
|
350
|
+
let cache = capabilityProbes.get(key);
|
|
351
|
+
if (!cache) {
|
|
352
|
+
cache = {};
|
|
353
|
+
capabilityProbes.set(key, cache);
|
|
354
|
+
}
|
|
355
|
+
// Any run that can launch child lanes needs verified per-session permission
|
|
356
|
+
// enforcement: read-only child lanes are contained by a deny-by-default
|
|
357
|
+
// permission ruleset (see resolveLanePolicy in authority-policy.js), so the
|
|
358
|
+
// runtime must prove it enforces child-session permission rules before the
|
|
359
|
+
// first lane spawns. This is independent of elevated authority — a read-only
|
|
360
|
+
// child-capable run needs the gate just as much as a shell/edit run. Elevated
|
|
361
|
+
// authority adds the same requirement for the usual containment reasons.
|
|
362
|
+
const elevated = authority.shell || authority.network || authority.mcp || authority.edit || authority.worktreeEdit || authority.integration;
|
|
363
|
+
const needsPermissions = elevated || options.childLanesAllowed === true;
|
|
364
|
+
const needsWorktree = authority.edit || authority.worktreeEdit || authority.integration;
|
|
365
|
+
|
|
366
|
+
// A gate counts as a cacheable success only when it is actually verified; an
|
|
367
|
+
// "available-unverified" / "blocked" / "failed" gate is transient and re-probed.
|
|
368
|
+
const gateVerifiedResult = (gate) => gate?.verified === true;
|
|
369
|
+
// Honor forced gates (tests, or a prior in-process verification) before
|
|
370
|
+
// live-probing, consistent with liveGateReport's probeOrShape. This lets tests
|
|
371
|
+
// fake the gate via pluginContext.__workflowLiveGates and avoids a redundant
|
|
372
|
+
// live probe when a gate value is already known.
|
|
373
|
+
const forcedGates = pluginContext.__workflowLiveGates ?? {};
|
|
374
|
+
async function resolvePermissionGate() {
|
|
375
|
+
const forcedValue = forcedGate(forcedGates.permissionEnforcement);
|
|
376
|
+
if (forcedValue) return forcedValue;
|
|
377
|
+
return await cachedProbe(cache, "permissionEnforcement", () => probeDeniedBash(pluginContext, toolContext), gateVerifiedResult);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (forced.structuredOutput === undefined && adapter.capabilities.structuredOutput === "available-unverified") {
|
|
381
|
+
adapter.capabilities.structuredOutput = await cachedProbe(
|
|
382
|
+
cache,
|
|
383
|
+
"structuredOutput",
|
|
384
|
+
() => probeStructuredOutput(pluginContext, adapter),
|
|
385
|
+
(result) => result === "available",
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (needsPermissions && forced.permissions === undefined && adapter.capabilities.permissions === "available-unverified") {
|
|
389
|
+
const gate = await resolvePermissionGate();
|
|
390
|
+
setLiveGateDiagnostic(adapter, "permissionEnforcement", gate);
|
|
391
|
+
adapter.capabilities.permissions = gateCapability(gate);
|
|
392
|
+
}
|
|
393
|
+
if (needsWorktree && forced.worktree === undefined && adapter.capabilities.worktree === "available-unverified") {
|
|
394
|
+
const gate = await cachedProbe(cache, "worktreeApi", () => probeWorktreeGate(pluginContext, adapter), gateVerifiedResult);
|
|
395
|
+
setLiveGateDiagnostic(adapter, "worktreeApi", gate);
|
|
396
|
+
adapter.capabilities.worktree = gateCapability(gate);
|
|
397
|
+
}
|
|
398
|
+
if (needsWorktree && forced.directoryRooting === undefined && adapter.capabilities.directoryRooting === "available-unverified") {
|
|
399
|
+
const gate = await cachedProbe(cache, "directoryRooting", () => probeDirectoryRootingGate(pluginContext, toolContext), gateVerifiedResult);
|
|
400
|
+
setLiveGateDiagnostic(adapter, "directoryRooting", gate);
|
|
401
|
+
adapter.capabilities.directoryRooting = gateCapability(gate);
|
|
402
|
+
}
|
|
403
|
+
if (needsWorktree && forced.worktreeEditIsolation === undefined && adapter.capabilities.worktreeEditIsolation === "available-unverified") {
|
|
404
|
+
const gate = await cachedProbe(cache, "worktreeEditIsolation", () => probeWorktreeEditIsolationGate(pluginContext, toolContext, adapter), gateVerifiedResult);
|
|
405
|
+
setLiveGateDiagnostic(adapter, "worktreeEditIsolation", gate);
|
|
406
|
+
adapter.capabilities.worktreeEditIsolation = gateCapability(gate);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const VERIFIED_GATE_CAPABILITY_MAP = {
|
|
411
|
+
permissionEnforcement: "permissions",
|
|
412
|
+
structuredOutput: "structuredOutput",
|
|
413
|
+
worktreeApi: "worktree",
|
|
414
|
+
directoryRooting: "directoryRooting",
|
|
415
|
+
worktreeEditIsolation: "worktreeEditIsolation",
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
function promoteVerifiedGateCapabilities(adapter, gateStatus = {}) {
|
|
419
|
+
for (const [gateName, capabilityName] of Object.entries(VERIFIED_GATE_CAPABILITY_MAP)) {
|
|
420
|
+
if (gateStatus[gateName]?.verified === true) adapter.capabilities[capabilityName] = "available";
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// R32 (opencode-workflows-6ti): a gate that is `verified: true` but whose evidence is
|
|
425
|
+
// non-behavioral (currently only `in-process-smoke`) does NOT prove the target subsystem
|
|
426
|
+
// works. Surface it as a blocker for *required* authority gates so that adding e.g.
|
|
427
|
+
// backgroundContinuation to requiredGates is not auto-satisfied by a trivial event-loop
|
|
428
|
+
// yield. The operator can opt in per-strength via acceptWeakEvidence.
|
|
429
|
+
function weakEvidenceGateBlockers(gateStatus, acceptedStrengths) {
|
|
430
|
+
const accepted = new Set(acceptedStrengths ?? []);
|
|
431
|
+
return Object.entries(gateStatus)
|
|
432
|
+
.filter(([, gate]) => gate?.verified === true
|
|
433
|
+
&& NON_BEHAVIORAL_EVIDENCE_STRENGTHS.has(gate?.evidenceStrength)
|
|
434
|
+
&& !accepted.has(gate.evidenceStrength))
|
|
435
|
+
.map(([name, gate]) => `${name}=verified(${gate.evidenceStrength}) is non-behavioral and not accepted`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function verifyRequiredAuthorityGates(pluginContext, toolContext, adapter, authority, options = {}) {
|
|
439
|
+
const skipped = new Set(options.skipGates ?? []);
|
|
440
|
+
const required = normalizeRequiredGates((authority.requiredGates ?? []).filter((name) => !skipped.has(name)));
|
|
441
|
+
if (required.length === 0) return {};
|
|
442
|
+
const report = JSON.parse(await liveGateReport(pluginContext, toolContext, liveGateProbeArgsForNames(required)));
|
|
443
|
+
const gateStatus = compactLiveGateStatus(report, required);
|
|
444
|
+
adapter.diagnostics.liveGates = { ...(adapter.diagnostics.liveGates ?? {}), ...gateStatus };
|
|
445
|
+
const blockers = [
|
|
446
|
+
...nonVerifiedGateSummaries(gateStatus),
|
|
447
|
+
...weakEvidenceGateBlockers(gateStatus, options.acceptWeakEvidence),
|
|
448
|
+
];
|
|
449
|
+
if (blockers.length > 0) {
|
|
450
|
+
throw new Error(`Workflow authority profile ${authority.profile || AD_HOC_AUTHORITY_PROFILE} requires verified live gates: ${blockers.join(", ")}`);
|
|
451
|
+
}
|
|
452
|
+
return gateStatus;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Historical compatibility hook for network/MCP live-gate diagnostics. Launch-time
|
|
456
|
+
// enforcement for webfetch/websearch/mcp is owned by permissionRulesForAuthority
|
|
457
|
+
// plus the verified permissionEnforcement gate; networkAccess remains informational
|
|
458
|
+
// until a real behavioral probe exists. mcpAccess has an explicit opt-in live probe.
|
|
459
|
+
async function verifyNetworkMcpAuthorityGates() {
|
|
460
|
+
return {};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function hasLiveGateProbeFlags(args = {}) {
|
|
464
|
+
return [
|
|
465
|
+
"probePermissionEnforcement",
|
|
466
|
+
"probeDeniedBash",
|
|
467
|
+
"probeCommandScopedBash",
|
|
468
|
+
"probeSecretReadDeny",
|
|
469
|
+
"probeStructuredOutput",
|
|
470
|
+
"probeWorktreeApi",
|
|
471
|
+
"probeDirectoryRooting",
|
|
472
|
+
"probeWorktreeEditIsolation",
|
|
473
|
+
"probeIntegrationWorktreeIsolation",
|
|
474
|
+
"probeBackgroundContinuation",
|
|
475
|
+
"probeConcurrencyCapacity",
|
|
476
|
+
"probeCancellation",
|
|
477
|
+
"probeWorkflowNotification",
|
|
478
|
+
"probeNetworkAccess",
|
|
479
|
+
"probeMcpAccess",
|
|
480
|
+
].some((key) => args[key] === true);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function hardConcurrencyLimitForContext(pluginContext) {
|
|
484
|
+
return normalizeHardConcurrencyLimit(pluginContext?.__workflowHardConcurrencyLimit, HARD_CONCURRENCY_LIMIT);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function concurrencyProbeLimitForArgs(pluginContext, args = {}) {
|
|
488
|
+
const hardLimit = hardConcurrencyLimitForContext(pluginContext);
|
|
489
|
+
const requested = Number.isInteger(args.concurrencyProbeLimit) && args.concurrencyProbeLimit > 0
|
|
490
|
+
? args.concurrencyProbeLimit
|
|
491
|
+
: Math.min(DEFAULT_CONCURRENCY_PROBE_LIMIT, hardLimit);
|
|
492
|
+
return Math.max(1, Math.min(requested, hardLimit));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function assertLiveGateProbeAllowed(context, args = {}) {
|
|
496
|
+
// resetProbeCache mutates shared module state (forces re-probing), so it is gated
|
|
497
|
+
// exactly like a live probe: write-allowed context + explicit probe approvalIntent.
|
|
498
|
+
if (!hasLiveGateProbeFlags(args) && args.resetProbeCache !== true) return;
|
|
499
|
+
assertWriteWorkflowAllowed(context, "workflow_live_gates probes");
|
|
500
|
+
if (args.approvalIntent !== "probe") {
|
|
501
|
+
throw new WorkflowAuthorityError('workflow_live_gates probe flags require approvalIntent: "probe"');
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function liveGateReport(pluginContext, context, args = {}) {
|
|
506
|
+
const adapter = await createCapabilityAdapter(pluginContext);
|
|
507
|
+
const forced = pluginContext.__workflowLiveGates ?? {};
|
|
508
|
+
const session = sessionApi(pluginContext);
|
|
509
|
+
// R14 recovery hook: invalidate any cached probe promises so a subsequent
|
|
510
|
+
// promotion / probe re-runs from scratch instead of inheriting a stale transient
|
|
511
|
+
// failure. Scope to this runtime's serverUrl+directory key by default; a truthy
|
|
512
|
+
// non-"key" value (e.g. "all") clears the whole module cache.
|
|
513
|
+
let probeCacheCleared;
|
|
514
|
+
if (args.resetProbeCache === true) {
|
|
515
|
+
const key = `${redactServerUrl(pluginContext.serverUrl)}:${path.resolve(context.worktree || context.directory || ".")}`;
|
|
516
|
+
probeCacheCleared = args.resetProbeCacheScope === "all"
|
|
517
|
+
? { scope: "all", cleared: invalidateCapabilityProbes() }
|
|
518
|
+
: { scope: "runtime", key, cleared: invalidateCapabilityProbes(key) };
|
|
519
|
+
}
|
|
520
|
+
async function probeOrShape(name, probeFlag, available, evidence, probe) {
|
|
521
|
+
const forcedValue = forcedGate(forced[name]);
|
|
522
|
+
if (forcedValue) return forcedValue;
|
|
523
|
+
if (probeFlag) return await probe();
|
|
524
|
+
return shapeGate(undefined, available, evidence);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const gates = {
|
|
528
|
+
permissionEnforcement: await probeOrShape(
|
|
529
|
+
"permissionEnforcement",
|
|
530
|
+
args.probePermissionEnforcement === true || args.probeDeniedBash === true,
|
|
531
|
+
adapter.capabilities.permissions !== "unavailable",
|
|
532
|
+
"session.create can accept permission rules; enforcement still needs a live denial probe",
|
|
533
|
+
() => probeDeniedBash(pluginContext, context),
|
|
534
|
+
),
|
|
535
|
+
commandScopedBash: await probeOrShape(
|
|
536
|
+
"commandScopedBash",
|
|
537
|
+
args.probeCommandScopedBash === true,
|
|
538
|
+
adapter.capabilities.permissions !== "unavailable",
|
|
539
|
+
"permission rules can be configured; command-scoped bash denial still needs a live probe",
|
|
540
|
+
() => probeCommandScopedBash(pluginContext, context),
|
|
541
|
+
),
|
|
542
|
+
secretReadDeny: await probeOrShape(
|
|
543
|
+
"secretReadDeny",
|
|
544
|
+
args.probeSecretReadDeny === true,
|
|
545
|
+
adapter.capabilities.permissions !== "unavailable",
|
|
546
|
+
"read deny rules can be configured; secret-read denial still needs a live probe",
|
|
547
|
+
() => probeSecretReadDeny(pluginContext, context),
|
|
548
|
+
),
|
|
549
|
+
structuredOutput: await probeOrShape(
|
|
550
|
+
"structuredOutput",
|
|
551
|
+
args.probeStructuredOutput === true,
|
|
552
|
+
adapter.capabilities.structuredOutput !== "unavailable",
|
|
553
|
+
"session.prompt can accept format; schema compliance still needs a live structured-output probe",
|
|
554
|
+
() => probeStructuredOutputGate(pluginContext, adapter),
|
|
555
|
+
),
|
|
556
|
+
worktreeApi: await probeOrShape(
|
|
557
|
+
"worktreeApi",
|
|
558
|
+
args.probeWorktreeApi === true,
|
|
559
|
+
adapter.capabilities.worktree !== "unavailable",
|
|
560
|
+
"worktree create/remove API shape is present; isolation still needs a live worktree probe",
|
|
561
|
+
() => probeWorktreeGate(pluginContext, adapter),
|
|
562
|
+
),
|
|
563
|
+
directoryRooting: await probeOrShape(
|
|
564
|
+
"directoryRooting",
|
|
565
|
+
args.probeDirectoryRooting === true,
|
|
566
|
+
adapter.capabilities.directoryRooting !== "unavailable",
|
|
567
|
+
"child session directory rooting appears available; edit isolation still needs a live rooted child probe",
|
|
568
|
+
() => probeDirectoryRootingGate(pluginContext, context),
|
|
569
|
+
),
|
|
570
|
+
worktreeEditIsolation: await probeOrShape(
|
|
571
|
+
"worktreeEditIsolation",
|
|
572
|
+
args.probeWorktreeEditIsolation === true,
|
|
573
|
+
adapter.capabilities.worktree !== "unavailable" && adapter.capabilities.directoryRooting !== "unavailable",
|
|
574
|
+
"worktree and directory-rooting API shapes are present; edit isolation still needs a live worktree probe",
|
|
575
|
+
() => probeWorktreeEditIsolationGate(pluginContext, context, adapter),
|
|
576
|
+
),
|
|
577
|
+
integrationWorktreeIsolation: await probeOrShape(
|
|
578
|
+
"integrationWorktreeIsolation",
|
|
579
|
+
args.probeIntegrationWorktreeIsolation === true,
|
|
580
|
+
adapter.capabilities.directoryRooting !== "unavailable",
|
|
581
|
+
"local Git integration worktree isolation still needs a live scratch-repo probe",
|
|
582
|
+
() => probeIntegrationWorktreeIsolationGate(pluginContext, context),
|
|
583
|
+
),
|
|
584
|
+
backgroundContinuation: await probeOrShape(
|
|
585
|
+
"backgroundContinuation",
|
|
586
|
+
args.probeBackgroundContinuation === true,
|
|
587
|
+
adapter.capabilities.backgroundContinuation === "available",
|
|
588
|
+
"plugin can schedule background work in-process; long-run survival still needs live observation",
|
|
589
|
+
() => probeBackgroundContinuationGate(),
|
|
590
|
+
),
|
|
591
|
+
concurrencyCapacity: await probeOrShape(
|
|
592
|
+
"concurrencyCapacity",
|
|
593
|
+
args.probeConcurrencyCapacity === true,
|
|
594
|
+
session.has("create") && session.has("prompt"),
|
|
595
|
+
`session.create/session.prompt APIs are present; DEFAULT_CONCURRENCY remains ${DEFAULT_CONCURRENCY} until a live burst probe characterizes this runtime`,
|
|
596
|
+
() => probeConcurrencyCapacityGate(pluginContext, context, { limit: concurrencyProbeLimitForArgs(pluginContext, args) }),
|
|
597
|
+
),
|
|
598
|
+
cancellation: await probeOrShape(
|
|
599
|
+
"cancellation",
|
|
600
|
+
args.probeCancellation === true,
|
|
601
|
+
session.has("abort"),
|
|
602
|
+
"session.abort API shape is present; active-child cancellation still needs a live probe",
|
|
603
|
+
() => probeCancellationGate(pluginContext, context),
|
|
604
|
+
),
|
|
605
|
+
workflowCompletionNotification: await probeOrShape(
|
|
606
|
+
"workflowCompletionNotification",
|
|
607
|
+
args.probeWorkflowNotification === true,
|
|
608
|
+
session.has("promptAsync"),
|
|
609
|
+
"session.promptAsync API shape is present; idle-gated workflow notification delivery still needs an explicit probe",
|
|
610
|
+
() => probeWorkflowNotificationGate(pluginContext, context),
|
|
611
|
+
),
|
|
612
|
+
networkAccess: forcedGate(forced.networkAccess) ?? (args.probeNetworkAccess === true
|
|
613
|
+
? gateBlocked("network behavioral live-gate probe is reserved; networked authority cannot be verified by this runtime yet")
|
|
614
|
+
: gateAvailableUnverified("network permission policy can allow webfetch/websearch, but no behavioral live-gate probe exists yet; networked profiles remain reserved")),
|
|
615
|
+
mcpAccess: await probeOrShape(
|
|
616
|
+
"mcpAccess",
|
|
617
|
+
args.probeMcpAccess === true,
|
|
618
|
+
adapter.capabilities.permissions !== "unavailable",
|
|
619
|
+
"MCP permission rules can be configured; MCP allow/deny still needs a live probe",
|
|
620
|
+
() => probeMcpAccessGate(pluginContext, context),
|
|
621
|
+
),
|
|
622
|
+
};
|
|
623
|
+
const configured = Object.values(gates).every((value) => value.state !== "blocked");
|
|
624
|
+
const verified = Object.values(gates).every((value) => value.verified === true);
|
|
625
|
+
const report = {
|
|
626
|
+
configured,
|
|
627
|
+
verified,
|
|
628
|
+
...(probeCacheCleared ? { probeCacheCleared } : {}),
|
|
629
|
+
gates,
|
|
630
|
+
diagnostics: adapter.diagnostics,
|
|
631
|
+
note: "available-unverified means the API shape is present only; it is not live proof. Use explicit live probes to mark gates verified. A verified gate's evidenceStrength field distinguishes directly-observed target behavior (\"observed\") from weaker evidence: \"no-attempt-fallback\" verifies only that retained deny rules held and no successful tool call occurred (not equivalent to an observed denial); \"in-process-smoke\" verifies only in-process event-loop yield and does not exercise the OpenCode background subsystem or imply restart survival (used by background-continuation). None of the weaker strengths are equivalent to an observed denial, an observed tool result, or observed OpenCode continuation. directory-rooting verifies ONLY on a deterministic sentinel read tool result; a child that merely echoes the cwd in text is reported as available-unverified (verified=false) and does not satisfy the required directoryRooting gate.",
|
|
632
|
+
};
|
|
633
|
+
if (args.format === "json") return JSON.stringify(report, null, 2);
|
|
634
|
+
const lines = Object.entries(gates).map(([key, value]) => `${key}: ${value.state}${value.verified ? " (verified)" : ""}`);
|
|
635
|
+
if (probeCacheCleared) lines.unshift(`probe cache cleared: ${probeCacheCleared.cleared} entr${probeCacheCleared.cleared === 1 ? "y" : "ies"} (${probeCacheCleared.scope})`);
|
|
636
|
+
return lines.join("\n");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function compactLiveGateStatus(report, names = Object.keys(report?.gates ?? {})) {
|
|
640
|
+
return Object.fromEntries(names.map((name) => {
|
|
641
|
+
const gate = report?.gates?.[name] ?? gateBlocked("gate missing from live-gate report");
|
|
642
|
+
const summary = { state: gate.state, verified: gate.verified === true, evidence: gate.evidence };
|
|
643
|
+
if (gate.evidenceStrength !== undefined) summary.evidenceStrength = gate.evidenceStrength;
|
|
644
|
+
return [name, summary];
|
|
645
|
+
}));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function gateCapability(gate) {
|
|
649
|
+
if (gate?.verified === true) return "available";
|
|
650
|
+
if (gate?.state === "blocked") return "unavailable";
|
|
651
|
+
return "available-unverified";
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function setLiveGateDiagnostic(adapter, name, gate) {
|
|
655
|
+
adapter.diagnostics.liveGates = { ...(adapter.diagnostics.liveGates ?? {}), [name]: gate };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function liveGateProbeArgsForNames(names) {
|
|
659
|
+
const args = { format: "json" };
|
|
660
|
+
for (const name of names) {
|
|
661
|
+
if (name === "permissionEnforcement") args.probePermissionEnforcement = true;
|
|
662
|
+
else if (name === "commandScopedBash") args.probeCommandScopedBash = true;
|
|
663
|
+
else if (name === "secretReadDeny") args.probeSecretReadDeny = true;
|
|
664
|
+
else if (name === "structuredOutput") args.probeStructuredOutput = true;
|
|
665
|
+
else if (name === "worktreeApi") args.probeWorktreeApi = true;
|
|
666
|
+
else if (name === "directoryRooting") args.probeDirectoryRooting = true;
|
|
667
|
+
else if (name === "worktreeEditIsolation") args.probeWorktreeEditIsolation = true;
|
|
668
|
+
else if (name === "integrationWorktreeIsolation") args.probeIntegrationWorktreeIsolation = true;
|
|
669
|
+
else if (name === "backgroundContinuation") args.probeBackgroundContinuation = true;
|
|
670
|
+
else if (name === "concurrencyCapacity") args.probeConcurrencyCapacity = true;
|
|
671
|
+
else if (name === "cancellation") args.probeCancellation = true;
|
|
672
|
+
else if (name === "workflowCompletionNotification") args.probeWorkflowNotification = true;
|
|
673
|
+
else if (name === "networkAccess") args.probeNetworkAccess = true;
|
|
674
|
+
else if (name === "mcpAccess") args.probeMcpAccess = true;
|
|
675
|
+
}
|
|
676
|
+
return args;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function nonVerifiedGateSummaries(gateStatus) {
|
|
680
|
+
return Object.entries(gateStatus).filter(([, gate]) => gate?.verified !== true)
|
|
681
|
+
.map(([name, gate]) => `${name}=${gate?.state ?? "missing"}`);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export {
|
|
685
|
+
NON_BEHAVIORAL_EVIDENCE_STRENGTHS,
|
|
686
|
+
capabilityProbes,
|
|
687
|
+
BoundedProbeCache,
|
|
688
|
+
CAPABILITY_PROBE_CACHE_MAX,
|
|
689
|
+
VERIFIED_PROBE_TTL_MS,
|
|
690
|
+
redactServerUrl,
|
|
691
|
+
createCapabilityAdapter,
|
|
692
|
+
cachedProbe,
|
|
693
|
+
invalidateCapabilityProbes,
|
|
694
|
+
promoteCapabilities,
|
|
695
|
+
promoteVerifiedGateCapabilities,
|
|
696
|
+
weakEvidenceGateBlockers,
|
|
697
|
+
verifyRequiredAuthorityGates,
|
|
698
|
+
verifyNetworkMcpAuthorityGates,
|
|
699
|
+
hasLiveGateProbeFlags,
|
|
700
|
+
assertLiveGateProbeAllowed,
|
|
701
|
+
liveGateReport,
|
|
702
|
+
compactLiveGateStatus,
|
|
703
|
+
gateCapability,
|
|
704
|
+
setLiveGateDiagnostic,
|
|
705
|
+
liveGateProbeArgsForNames,
|
|
706
|
+
nonVerifiedGateSummaries,
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// Re-export the gate-shape constructors (now in gate-shapes.js) and the live-gate
|
|
710
|
+
// probe functions + probe-only helpers (now in live-gate-probes.js) so the public
|
|
711
|
+
// CapabilityAdapter surface and the kernel barrel (index.js `export *`) are unchanged.
|
|
712
|
+
export {
|
|
713
|
+
forcedGate,
|
|
714
|
+
gateAvailableUnverified,
|
|
715
|
+
gateBlocked,
|
|
716
|
+
gateFailed,
|
|
717
|
+
gateVerified,
|
|
718
|
+
shapeGate,
|
|
719
|
+
transportFailureGate,
|
|
720
|
+
} from "./gate-shapes.js";
|
|
721
|
+
|
|
722
|
+
export {
|
|
723
|
+
collectTextParts,
|
|
724
|
+
collectToolParts,
|
|
725
|
+
createdSessionRetainedPermission,
|
|
726
|
+
denialProbeResult,
|
|
727
|
+
deterministicToolProbeResult,
|
|
728
|
+
initScratchGitRepo,
|
|
729
|
+
isDenialEvidence,
|
|
730
|
+
liveProbeTimeoutMs,
|
|
731
|
+
opencodeChildPermissionDenyRules,
|
|
732
|
+
probeBackgroundContinuationGate,
|
|
733
|
+
probeCancellationGate,
|
|
734
|
+
probeCommandScopedBash,
|
|
735
|
+
probeConcurrencyCapacityGate,
|
|
736
|
+
probeDeniedBash,
|
|
737
|
+
probeDirectoryRootingGate,
|
|
738
|
+
probeIntegrationWorktreeIsolationGate,
|
|
739
|
+
probeMcpAccessGate,
|
|
740
|
+
probeSecretReadDeny,
|
|
741
|
+
probeStructuredOutput,
|
|
742
|
+
probeStructuredOutputGate,
|
|
743
|
+
probeWorkflowNotificationGate,
|
|
744
|
+
probeWorktree,
|
|
745
|
+
probeWorktreeEditIsolationGate,
|
|
746
|
+
probeWorktreeGate,
|
|
747
|
+
removeGitWorktreeForce,
|
|
748
|
+
sessionMessagesPayloadForProbe,
|
|
749
|
+
toolPartName,
|
|
750
|
+
unwrapClientResult,
|
|
751
|
+
valueContainsString,
|
|
752
|
+
withLiveProbeTimeout,
|
|
753
|
+
} from "./live-gate-probes.js";
|