@aiwg/cli 2026.10.10 → 2026.10.12
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/agentic/code/providers/model-capabilities.v1.json +78 -14
- package/agentic/code/providers/model-catalog.v1.json +62 -12
- package/dist/src/artifacts/binding-target-identity.js +51 -0
- package/dist/src/artifacts/checksum-manifest.js +12 -0
- package/dist/src/artifacts/index-builder.js +11 -13
- package/dist/src/artifacts/query-engine.js +5 -11
- package/dist/src/cli/handlers/doctor-help.js +32 -0
- package/dist/src/cli/handlers/gates.js +19 -18
- package/dist/src/cli/handlers/models-help.js +7 -1
- package/dist/src/cli/handlers/models.js +3 -3
- package/dist/src/cli/handlers/script-runner.js +8 -5
- package/dist/src/cli/handlers/sessions-intake.js +104 -0
- package/dist/src/cli/handlers/sessions-tail.js +172 -0
- package/dist/src/cli/handlers/sessions.js +287 -31
- package/dist/src/cli/handlers/skill-lint.js +10 -3
- package/dist/src/cli/handlers/steward-format.js +100 -0
- package/dist/src/cli/handlers/steward-models.js +167 -0
- package/dist/src/cli/handlers/steward.js +14 -248
- package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
- package/dist/src/cli/handlers/use.js +18 -28
- package/dist/src/cli/handlers/utilities.js +1 -24
- package/dist/src/config/aiwg-config.js +7 -0
- package/dist/src/config/authorization-cli.js +89 -0
- package/dist/src/config/cli-help.js +1 -0
- package/dist/src/config/cli.js +10 -1
- package/dist/src/config/worker-context.js +337 -0
- package/dist/src/decision/context-pruning.js +3 -2
- package/dist/src/decision/ensemble/contract.js +15 -4
- package/dist/src/decision/ensemble-study/artifacts.js +5 -0
- package/dist/src/decision/ensemble-study/score.js +4 -3
- package/dist/src/decision/feature-export.js +3 -9
- package/dist/src/decision/heldout/collector.js +2 -2
- package/dist/src/decision/issue-triage/runtime.js +3 -9
- package/dist/src/decision/qualification/gates.js +2 -1
- package/dist/src/decision/qualification/release.js +2 -3
- package/dist/src/decision/routing/report.js +2 -0
- package/dist/src/decision/sdlc-screening.js +3 -5
- package/dist/src/decision/sensitivity/comparative.js +5 -3
- package/dist/src/gates/driver.js +6 -77
- package/dist/src/gates/evaluate.js +4 -11
- package/dist/src/gates/floors.js +4 -4
- package/dist/src/gates/index.js +3 -3
- package/dist/src/gates/lattice.js +26 -0
- package/dist/src/gates/registry.js +2 -1
- package/dist/src/gates/report.js +19 -7
- package/dist/src/issues/audit-cli.js +120 -0
- package/dist/src/issues/audit-contract.js +64 -0
- package/dist/src/issues/audit-local.js +82 -0
- package/dist/src/issues/audit.js +220 -0
- package/dist/src/lint/rule-activation.js +15 -1
- package/dist/src/mcp/tools/worker-invoke.mjs +108 -10
- package/dist/src/mission-protocol/canonical-codecs.js +270 -0
- package/dist/src/mission-protocol/codecs.js +22 -550
- package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
- package/dist/src/mission-protocol/health-trace-types.js +43 -0
- package/dist/src/mission-protocol/types.js +2 -42
- package/dist/src/mission-protocol/worker-contract-json.js +10 -0
- package/dist/src/mission-protocol/worker-review-gate.js +100 -0
- package/dist/src/mission-protocol/worker-review-types.js +2 -0
- package/dist/src/models/catalog-policy.js +280 -0
- package/dist/src/models/index.js +1 -1
- package/dist/src/models/model-capabilities.v1.json +78 -14
- package/dist/src/models/model-catalog.v1.json +62 -12
- package/dist/src/models/policy-resources.js +28 -0
- package/dist/src/models/provider-policy.js +10 -539
- package/dist/src/models/resolver.js +26 -8
- package/dist/src/models/router.js +15 -2
- package/dist/src/models/tiers.js +13 -0
- package/dist/src/models/types.js +0 -6
- package/dist/src/models/worker-ceilings.js +59 -0
- package/dist/src/models/worker-invocation-policy.js +243 -0
- package/dist/src/models/worker-plan.js +16 -20
- package/dist/src/models/worker-review-lineage.js +55 -0
- package/dist/src/models/worker-selection-policy.js +1 -1
- package/dist/src/models/wrapper-route.js +6 -3
- package/dist/src/policy/authorization.js +30 -1
- package/dist/src/policy/hitl-config.js +63 -0
- package/dist/src/policy/hitl-receipt.js +57 -0
- package/dist/src/policy/hitl-types.js +4 -0
- package/dist/src/policy/hitl-validation.js +39 -0
- package/dist/src/policy/hitl.js +117 -0
- package/dist/src/process/aiwg-probe-command.d.mts +15 -0
- package/dist/src/process/aiwg-probe-command.mjs +37 -0
- package/dist/src/rules/activation.d.mts +1 -1
- package/dist/src/rules/activation.mjs +0 -2
- package/dist/src/rules/gate-coverage.js +51 -0
- package/dist/src/rules/gate-references.js +16 -0
- package/dist/src/rules/rule-frontmatter.schema.json +7 -1
- package/dist/src/serve/worker-command-request.js +21 -4
- package/dist/src/serve/worker-invocation-preparation.js +3 -1
- package/dist/src/serve/worker-invocation.js +21 -10
- package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
- package/dist/src/sessions/adapters/grok-build-native.js +269 -0
- package/dist/src/sessions/adapters/grok-build.js +57 -15
- package/dist/src/sessions/adapters/grokbot.js +7 -0
- package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
- package/dist/src/sessions/adapters/hermes.js +42 -3
- package/dist/src/sessions/adapters/warp.js +1 -1
- package/dist/src/sessions/batch-import.js +44 -6
- package/dist/src/sessions/contracts.js +20 -1
- package/dist/src/sessions/coverage-preflight.js +7 -1
- package/dist/src/sessions/discovery-capability.js +38 -0
- package/dist/src/sessions/fortemi-export-mapping.js +33 -3
- package/dist/src/sessions/host-catalog.js +157 -0
- package/dist/src/sessions/image-blobs.js +141 -0
- package/dist/src/sessions/importer.js +9 -232
- package/dist/src/sessions/index.js +6 -0
- package/dist/src/sessions/intake-dir.js +71 -0
- package/dist/src/sessions/normalization.js +251 -0
- package/dist/src/sessions/refresh.js +25 -2
- package/dist/src/sessions/repository.js +164 -22
- package/dist/src/sessions/tail.js +91 -0
- package/dist/src/sessions/workspace-discovery.js +88 -9
- package/dist/src/skills/agent-skill-sidecar.js +28 -0
- package/dist/src/skills/agent-skills.js +4 -23
- package/dist/src/skills/claude-surface-projection.js +25 -0
- package/dist/src/skills/deployer.js +8 -288
- package/dist/src/skills/deployment-policy.js +114 -0
- package/dist/src/skills/deployment-projection.js +152 -0
- package/dist/src/skills/deployment-types.js +20 -0
- package/dist/src/skills/exporter.js +8 -31
- package/dist/src/skills/provider-extensions.js +41 -0
- package/dist/src/skills/run.js +6 -0
- package/dist/src/skills/types.js +0 -8
- package/dist/src/skills/validator-resources.js +68 -0
- package/dist/src/skills/validator.js +6 -55
- package/dist/src/storage/cli-migrate-options.js +125 -0
- package/dist/src/storage/cli-migrate.js +111 -0
- package/dist/src/storage/cli.js +4 -279
- package/dist/src/storage/migration-adapter.js +79 -0
- package/dist/src/storage/migration-copy.js +108 -0
- package/dist/src/storage/migration-protocol.js +14 -197
- package/dist/src/storage/migration-records.js +186 -0
- package/dist/src/storage/migration-types.js +12 -0
- package/dist/src/tracker/forge-http.js +2 -1
- package/dist/src/tracker/issue-pages.js +110 -0
- package/package.json +7 -1
- package/schemas/mission-v1.schema.json +335 -0
- package/schemas/models/model-catalog.v1.schema.json +19 -1
- package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
- package/schemas/models/model-policy.v1.schema.json +1 -1
- package/tools/agents/providers/provider-paths-staging.mjs +35 -0
- package/tools/agents/providers/rule-binding-targets.mjs +146 -0
- package/tools/agents/providers/rule-delivery.mjs +7 -126
- package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
- package/tools/ralph-external/lib/worker-context.mjs +237 -0
- package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
- package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
- package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +17 -29
- package/tools/ralph-external/lib/worker-launch-cli.mjs +3 -1
- package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
- package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
- package/tools/ralph-external/lib/worker-termination.mjs +82 -0
- package/tools/ralph-external/session-launcher.mjs +51 -96
- package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { access, copyFile, mkdir, readFile, rename } from 'node:fs/promises';
|
|
10
10
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
11
11
|
import { load as loadYaml } from 'js-yaml';
|
|
12
|
+
export { resolveHitlAuthorization } from './hitl.js';
|
|
12
13
|
import { projectAiwgPath, projectControlPath } from '../config/project-artifacts.js';
|
|
13
14
|
/**
|
|
14
15
|
* Export provider mappings without inventing provider grants. Stable IDs,
|
|
@@ -170,13 +171,41 @@ const LEGACY_MANIFESTS = [
|
|
|
170
171
|
'.aiwg/ops/security/repo-access.manifest.yaml',
|
|
171
172
|
'.aiwg/security/repo-access.manifest.yaml',
|
|
172
173
|
];
|
|
174
|
+
/**
|
|
175
|
+
* Factory delivery defaults written by `aiwg init`/`aiwg use`
|
|
176
|
+
* (emptyConfig in src/config/aiwg-config.ts). Kept as a literal here —
|
|
177
|
+
* rather than importing the config module — because aiwg-config already
|
|
178
|
+
* imports this module for authorization validation, so a value import
|
|
179
|
+
* would be a cycle. A unit test pins this literal against
|
|
180
|
+
* emptyConfig().delivery (#3635).
|
|
181
|
+
*/
|
|
182
|
+
const DEFAULT_DELIVERY_POLICY = {
|
|
183
|
+
mode: 'pr-required',
|
|
184
|
+
default_branch: 'main',
|
|
185
|
+
require_ci_green: true,
|
|
186
|
+
auto_close_issues: true,
|
|
187
|
+
issue_comment_on_cycle: true,
|
|
188
|
+
force_push_policy: 'never',
|
|
189
|
+
};
|
|
190
|
+
/** True when the delivery block is exactly what the tool writes for a fresh project. */
|
|
191
|
+
export function isDefaultDeliveryPolicy(delivery) {
|
|
192
|
+
if (!delivery || typeof delivery !== 'object')
|
|
193
|
+
return false;
|
|
194
|
+
const actual = delivery;
|
|
195
|
+
const actualKeys = Object.keys(actual);
|
|
196
|
+
return actualKeys.length === Object.keys(DEFAULT_DELIVERY_POLICY).length
|
|
197
|
+
&& actualKeys.every((key) => actual[key] === DEFAULT_DELIVERY_POLICY[key]);
|
|
198
|
+
}
|
|
173
199
|
export async function auditLegacyPermissions(projectDir, config) {
|
|
174
200
|
const out = validateAuthorization(config?.authorization);
|
|
175
201
|
if (!config?.authorization && config?.repos?.some(repo => repo.allowed?.length))
|
|
176
202
|
out.push({ severity: 'warning', code: 'legacy-repos-allowed', source: '.aiwg/aiwg.config', message: 'repos[].allowed should be normalized into authorization roles and assignments.' });
|
|
177
203
|
if (!config?.authorization && Object.keys(config?.repo_maintainer?.tiers ?? {}).length)
|
|
178
204
|
out.push({ severity: 'warning', code: 'legacy-maintainer-tiers', source: '.aiwg/aiwg.config', message: 'repo_maintainer.tiers should be represented by normalized role assignments.' });
|
|
179
|
-
|
|
205
|
+
// Tool-written delivery defaults are not operator legacy input: a fresh
|
|
206
|
+
// `aiwg use` must not produce something that needs migration (#3635).
|
|
207
|
+
// Operator-edited delivery (any key added, removed, or changed) still flags.
|
|
208
|
+
if (!config?.authorization && config?.delivery && !isDefaultDeliveryPolicy(config.delivery))
|
|
180
209
|
out.push({ severity: 'warning', code: 'legacy-delivery-policy', source: '.aiwg/aiwg.config', message: 'delivery and signing operations should be normalized into scoped role assignments.' });
|
|
181
210
|
for (const path of LEGACY_MANIFESTS) {
|
|
182
211
|
try {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Loads governing workspace/member policies without merging or broadening their grants.
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
4
|
+
import { getConfigPath, readAiwgConfigCandidate } from '../config/aiwg-config.js';
|
|
5
|
+
export function ensureAuthorization(config) {
|
|
6
|
+
return config.authorization ??= { version: '1', default_effect: 'deny', resources: {}, permissions: {}, roles: {}, assignments: [] };
|
|
7
|
+
}
|
|
8
|
+
const inside = (child, root) => {
|
|
9
|
+
const path = relative(root, child);
|
|
10
|
+
return path === '' || (path !== '..' && !path.startsWith('../') && !isAbsolute(path));
|
|
11
|
+
};
|
|
12
|
+
async function canonicalDirectory(path) {
|
|
13
|
+
try {
|
|
14
|
+
return await realpath(path);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error.code !== 'ENOENT')
|
|
18
|
+
throw error;
|
|
19
|
+
const parent = dirname(path);
|
|
20
|
+
if (parent === path)
|
|
21
|
+
throw error;
|
|
22
|
+
return resolve(await canonicalDirectory(parent), basename(path));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Each config boundary narrows grants. A member without HITL policy cannot inherit approval. */
|
|
26
|
+
export async function loadHitlLayers(start) {
|
|
27
|
+
const layers = [];
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
let current = await canonicalDirectory(resolve(start));
|
|
30
|
+
let projectDir;
|
|
31
|
+
let child;
|
|
32
|
+
while (true) {
|
|
33
|
+
if (seen.has(current))
|
|
34
|
+
throw new Error('Cyclic HITL workspace membership');
|
|
35
|
+
seen.add(current);
|
|
36
|
+
const config = await readAiwgConfigCandidate(current);
|
|
37
|
+
if (config) {
|
|
38
|
+
projectDir ??= current;
|
|
39
|
+
if (child && config.repos) {
|
|
40
|
+
const root = await canonicalDirectory(resolve(current, config.workspace?.root ?? '.'));
|
|
41
|
+
const members = await Promise.all(config.repos.map(repo => canonicalDirectory(resolve(root, repo.path))));
|
|
42
|
+
if (!members.some(member => inside(child, member))) {
|
|
43
|
+
throw new Error('HITL workspace does not declare this member');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
layers.unshift({ source: getConfigPath(current), policy: config.authorization?.hitl });
|
|
47
|
+
child = current;
|
|
48
|
+
if (config.workspace?.member_of) {
|
|
49
|
+
current = await canonicalDirectory(resolve(current, config.workspace.member_of));
|
|
50
|
+
const parent = await readAiwgConfigCandidate(current);
|
|
51
|
+
if (!parent?.repos)
|
|
52
|
+
throw new Error('HITL workspace.member_of must name a workspace manifest');
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const parent = dirname(current);
|
|
57
|
+
if (parent === current)
|
|
58
|
+
break;
|
|
59
|
+
current = parent;
|
|
60
|
+
}
|
|
61
|
+
return { projectDir: projectDir ?? resolve(start), layers };
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=hitl-config.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Records a credential-free decision artifact through existing effect intent/reconcile receipts.
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { projectAiwgWritePath, resolveProjectAiwgDirForWrite } from '../config/project-artifacts.js';
|
|
5
|
+
import { recordIntent, reconcileEffect } from '../effects/ledger.js';
|
|
6
|
+
import { payloadDigest } from '../effects/identity.js';
|
|
7
|
+
import { assertDigestOnly } from '../effects/redaction.js';
|
|
8
|
+
import { fileDigestVerifier } from '../effects/verifiers/file.js';
|
|
9
|
+
import { createVerifierRegistry } from '../effects/verifiers/registry.js';
|
|
10
|
+
import { hitlDigest, resolveHitlAuthorization, validHitlAction } from './hitl.js';
|
|
11
|
+
import { HITL_CATEGORIES, HITL_ENVIRONMENTS } from './hitl-types.js';
|
|
12
|
+
/** Resolve again at receipt time. A preview result supplied by an agent is never trusted. */
|
|
13
|
+
export async function recordHitlAuthorization(ledger, action, context) {
|
|
14
|
+
const resolution = resolveHitlAuthorization(action, context);
|
|
15
|
+
const receipt = {
|
|
16
|
+
kind: 'hitl-authorization', evaluatedAtDigest: hitlDigest(context.now),
|
|
17
|
+
sessionGrantDigest: hitlDigest(context.sessionAuthorization ?? null), ...resolution,
|
|
18
|
+
// Arbitrary user strings become digests, including paths, repo identity and profile names.
|
|
19
|
+
matchedProfile: resolution.matchedProfile ? hitlDigest(resolution.matchedProfile) : null,
|
|
20
|
+
action: validHitlAction(action) ? {
|
|
21
|
+
category: HITL_CATEGORIES.find(category => category === action.category) ?? 'restricted',
|
|
22
|
+
descriptorDigest: hitlDigest(action), categoryDigest: hitlDigest(action.category),
|
|
23
|
+
reversible: action.reversible, impact: action.impact,
|
|
24
|
+
} : { descriptorDigest: hitlDigest(action) },
|
|
25
|
+
scope: validHitlAction(action) ? {
|
|
26
|
+
environmentClass: HITL_ENVIRONMENTS.find(environment => environment === action.environment) ?? 'restricted',
|
|
27
|
+
repoDigest: hitlDigest(action.repo), environmentDigest: hitlDigest(action.environment),
|
|
28
|
+
resourceDigests: action.resources.map(hitlDigest),
|
|
29
|
+
} : null,
|
|
30
|
+
origins: context.layers.map(layer => ({ sourceDigest: hitlDigest(layer.source), policyDigest: hitlDigest(layer.policy ?? null) })),
|
|
31
|
+
};
|
|
32
|
+
assertDigestOnly(receipt);
|
|
33
|
+
const bytes = JSON.stringify(receipt) + '\n';
|
|
34
|
+
const digest = payloadDigest(bytes);
|
|
35
|
+
const root = resolveProjectAiwgDirForWrite(ledger.projectDir);
|
|
36
|
+
const relativePath = join('effects', 'authorization', `${digest.slice(7)}.json`);
|
|
37
|
+
const file = projectAiwgWritePath(ledger.projectDir, relativePath);
|
|
38
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
39
|
+
try {
|
|
40
|
+
await writeFile(file, bytes, { flag: 'wx', mode: 0o600 });
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.code !== 'EEXIST')
|
|
44
|
+
throw error;
|
|
45
|
+
if (await readFile(file, 'utf8') !== bytes)
|
|
46
|
+
throw new Error('HITL receipt identity conflict');
|
|
47
|
+
}
|
|
48
|
+
const intent = await recordIntent(ledger, {
|
|
49
|
+
kind: 'file.digest', target: `file:${file}@${digest}`, payloadDigest: digest,
|
|
50
|
+
context: { policyVersion: resolution.policyVersion, decision: resolution.decision, provenance: resolution.provenance, receiptDigest: digest },
|
|
51
|
+
});
|
|
52
|
+
const reconciled = await reconcileEffect(ledger, intent.effectId, {
|
|
53
|
+
verifiers: createVerifierRegistry([fileDigestVerifier({ root })]),
|
|
54
|
+
});
|
|
55
|
+
return { resolution, file, intent, reconciled };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=hitl-receipt.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Defines bounded HITL grants and trusted resolver inputs, separate from RBAC.
|
|
2
|
+
export const HITL_CATEGORIES = ['repo.read', 'repo.edit', 'docs.edit', 'test.run', 'testnet.configure'];
|
|
3
|
+
export const HITL_ENVIRONMENTS = ['local', 'development', 'test', 'testnet'];
|
|
4
|
+
//# sourceMappingURL=hitl-types.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Validates HITL with the existing config schema definitions, shared by loader and resolver.
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import Ajv from 'ajv';
|
|
4
|
+
import addFormats from 'ajv-formats';
|
|
5
|
+
function compiled() {
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
let schema;
|
|
8
|
+
// Source and compiled modules share the shipped config schema; no cwd or home lookup.
|
|
9
|
+
for (const relative of ['../../vscode-extension/schemas/aiwg.config.v1.json',
|
|
10
|
+
'../../../vscode-extension/schemas/aiwg.config.v1.json']) {
|
|
11
|
+
try {
|
|
12
|
+
schema = require(relative);
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error?.code !== 'MODULE_NOT_FOUND')
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!schema)
|
|
21
|
+
throw new Error('HITL config schema unavailable');
|
|
22
|
+
const ajv = new Ajv({ strict: true, allErrors: true });
|
|
23
|
+
addFormats(ajv);
|
|
24
|
+
return ajv.compile({
|
|
25
|
+
$ref: '#/definitions/HitlPolicy',
|
|
26
|
+
definitions: Object.fromEntries(['HitlPolicy', 'HitlProfile', 'HitlSelector'].map(key => [key, schema.definitions[key]])),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// Compile at module initialization; resolving an action performs no filesystem I/O.
|
|
30
|
+
const validator = compiled();
|
|
31
|
+
export function validateHitlPolicy(value) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return [];
|
|
34
|
+
if (validator(value))
|
|
35
|
+
return [];
|
|
36
|
+
// Never echo supplied values, which may contain credentials or injected text.
|
|
37
|
+
return ['authorization.hitl: invalid bounded policy (scope, categories, expiry or unknown fields)'];
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=hitl-validation.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Resolves bounded HITL authorization without I/O, clocks, or prompt-text authority.
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { HITL_CATEGORIES, HITL_ENVIRONMENTS } from './hitl-types.js';
|
|
4
|
+
import { validateHitlPolicy } from './hitl-validation.js';
|
|
5
|
+
export function hitlDigest(value) {
|
|
6
|
+
const canonical = (v) => Array.isArray(v) ? v.map(canonical)
|
|
7
|
+
: v && typeof v === 'object' ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== undefined)
|
|
8
|
+
.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, x]) => [k, canonical(x)])) : v;
|
|
9
|
+
return `sha256:${createHash('sha256').update(JSON.stringify(canonical(value))).digest('hex')}`;
|
|
10
|
+
}
|
|
11
|
+
export const hitlPolicyVersion = (layers) => hitlDigest(layers);
|
|
12
|
+
const identifier = /^[A-Za-z0-9][A-Za-z0-9_.:/@-]{0,255}$/;
|
|
13
|
+
const pathValid = (path) => path.length > 0 && path.length <= 512 && !path.startsWith('/') && !path.includes(':')
|
|
14
|
+
&& !/[\\\x00-\x20*?\[\]{}!]/.test(path) && path.split('/').every(p => p !== '.' && p !== '..' && p !== '');
|
|
15
|
+
export function validHitlAction(value) {
|
|
16
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
17
|
+
return false;
|
|
18
|
+
return actionFieldsValid(value);
|
|
19
|
+
}
|
|
20
|
+
function actionFieldsValid(a) {
|
|
21
|
+
const keys = ['category', 'repo', 'environment', 'resources', 'reversible', 'impact'];
|
|
22
|
+
return Object.keys(a).length === keys.length && keys.every(k => Object.hasOwn(a, k))
|
|
23
|
+
&& [a.category, a.repo, a.environment].every(x => typeof x === 'string' && identifier.test(x))
|
|
24
|
+
&& Array.isArray(a.resources) && a.resources.length > 0 && a.resources.every(p => typeof p === 'string' && pathValid(p))
|
|
25
|
+
&& typeof a.reversible === 'boolean' && ['low', 'medium', 'high'].includes(a.impact);
|
|
26
|
+
}
|
|
27
|
+
/** Deliberately small glob language: * within a segment; ** crosses directories; ? one character. */
|
|
28
|
+
function glob(pattern, path) {
|
|
29
|
+
// Memoized matching bounds work by pattern length × path length, without regex backtracking.
|
|
30
|
+
const memo = new Map();
|
|
31
|
+
const match = (i, j) => {
|
|
32
|
+
const key = `${i}:${j}`;
|
|
33
|
+
if (memo.has(key))
|
|
34
|
+
return memo.get(key);
|
|
35
|
+
let result;
|
|
36
|
+
if (i === pattern.length)
|
|
37
|
+
result = j === path.length;
|
|
38
|
+
else if (pattern[i] === '*' && pattern[i + 1] === '*') {
|
|
39
|
+
if (pattern[i + 2] === '/') {
|
|
40
|
+
const slash = path.indexOf('/', j);
|
|
41
|
+
result = match(i + 3, j) || (slash >= 0 && match(i, slash + 1));
|
|
42
|
+
}
|
|
43
|
+
else
|
|
44
|
+
result = match(i + 2, j) || (j < path.length && match(i, j + 1));
|
|
45
|
+
}
|
|
46
|
+
else if (pattern[i] === '*')
|
|
47
|
+
result = match(i + 1, j)
|
|
48
|
+
|| (j < path.length && path[j] !== '/' && match(i, j + 1));
|
|
49
|
+
else
|
|
50
|
+
result = j < path.length && (pattern[i] === path[j] || (pattern[i] === '?' && path[j] !== '/'))
|
|
51
|
+
&& match(i + 1, j + 1);
|
|
52
|
+
memo.set(key, result);
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
return match(0, 0);
|
|
56
|
+
}
|
|
57
|
+
function selected(s, a) {
|
|
58
|
+
return (!s.categories || s.categories.includes(a.category)) && (!s.repos || s.repos.includes(a.repo))
|
|
59
|
+
&& (!s.environments || s.environments.includes(a.environment))
|
|
60
|
+
&& (!s.resources || a.resources.some(p => s.resources.some(g => glob(g, p))));
|
|
61
|
+
}
|
|
62
|
+
function grant(p, a, now) {
|
|
63
|
+
return now < Date.parse(p.expiresAt) && p.reversible === true && a.reversible
|
|
64
|
+
&& p.categories.some(c => c === a.category) && p.repos.includes(a.repo)
|
|
65
|
+
&& p.environments.some(e => e === a.environment)
|
|
66
|
+
&& a.resources.every(path => p.resources.some(g => glob(g, path)))
|
|
67
|
+
&& (a.impact === 'low' || (a.impact === 'medium' && p.maxImpact === 'medium'));
|
|
68
|
+
}
|
|
69
|
+
export function resolveHitlAuthorization(action, context) {
|
|
70
|
+
const policyVersion = hitlPolicyVersion(context.layers);
|
|
71
|
+
const result = (decision, reason, provenance, matchedProfile = null) => ({ decision, reason, policyVersion, matchedProfile, provenance });
|
|
72
|
+
if (context.platform)
|
|
73
|
+
return result(context.platform === 'deny' ? 'deny' : 'escalate', 'platform-constraint', 'platform');
|
|
74
|
+
if (!validHitlAction(action))
|
|
75
|
+
return result('escalate', 'invalid-action-descriptor', 'default');
|
|
76
|
+
if (!HITL_CATEGORIES.some(c => c === action.category) || !HITL_ENVIRONMENTS.some(e => e === action.environment)
|
|
77
|
+
|| !action.reversible || action.impact === 'high')
|
|
78
|
+
return result('escalate', 'safety-category-requires-explicit-review', 'platform');
|
|
79
|
+
// Revalidate even when invoked directly with objects that bypassed the config loader.
|
|
80
|
+
if (context.layers.some(l => validateHitlPolicy(l.policy).length))
|
|
81
|
+
return result('escalate', 'invalid-policy', 'default');
|
|
82
|
+
if (context.layers.some(l => l.policy?.deny.some(s => selected(s, action))))
|
|
83
|
+
return result('deny', 'explicit-deny', 'explicit-deny');
|
|
84
|
+
const now = Date.parse(context.now);
|
|
85
|
+
if (!Number.isFinite(now))
|
|
86
|
+
return result('escalate', 'invalid-clock', 'default');
|
|
87
|
+
const session = context.sessionAuthorization;
|
|
88
|
+
if (session?.source === 'session-user') {
|
|
89
|
+
const probe = { version: '1', enabled: true, reviewedAt: context.now, maxAgeSeconds: 1,
|
|
90
|
+
expiresAt: session.profile.expiresAt, profiles: { session: session.profile }, deny: [], requireReview: [] };
|
|
91
|
+
if (!validateHitlPolicy(probe).length && grant(session.profile, action, now))
|
|
92
|
+
return result('auto-authorized', 'session-user-authorization', 'session-user', 'session');
|
|
93
|
+
}
|
|
94
|
+
if (context.expectedPolicyVersion && policyVersion !== context.expectedPolicyVersion)
|
|
95
|
+
return result('escalate', 'stale-policy-version', 'project-policy');
|
|
96
|
+
if (!context.layers.length)
|
|
97
|
+
return result('escalate', 'no-project-policy', 'default');
|
|
98
|
+
let matched = null;
|
|
99
|
+
for (const layer of context.layers) {
|
|
100
|
+
const p = layer.policy;
|
|
101
|
+
if (!p?.enabled)
|
|
102
|
+
return result('escalate', p ? 'policy-revoked' : 'missing-member-policy', 'project-policy');
|
|
103
|
+
const reviewed = Date.parse(p.reviewedAt);
|
|
104
|
+
if (now >= Date.parse(p.expiresAt))
|
|
105
|
+
return result('escalate', 'policy-expired', 'project-policy');
|
|
106
|
+
if (reviewed > now || now - reviewed >= p.maxAgeSeconds * 1000)
|
|
107
|
+
return result('escalate', 'stale-policy-review', 'project-policy');
|
|
108
|
+
if (p.requireReview.some(s => selected(s, action)))
|
|
109
|
+
return result('escalate', 'policy-requires-review', 'project-policy');
|
|
110
|
+
const entry = Object.entries(p.profiles).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).find(([, profile]) => grant(profile, action, now));
|
|
111
|
+
if (!entry)
|
|
112
|
+
return result('escalate', 'outside-profile-or-expired', 'project-policy');
|
|
113
|
+
matched = entry[0];
|
|
114
|
+
}
|
|
115
|
+
return result('auto-authorized', 'bounded-project-policy', 'project-policy', matched);
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=hitl.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AiwgProbeCommand {
|
|
2
|
+
file: string;
|
|
3
|
+
prefix: string[];
|
|
4
|
+
via: 'aiwg-bin' | 'entrypoint' | 'package' | 'path';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function resolveAiwgProbeCommand(options?: {
|
|
8
|
+
env?: NodeJS.ProcessEnv;
|
|
9
|
+
argv1?: string | undefined;
|
|
10
|
+
execPath?: string;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
packageRoot?: string | null;
|
|
13
|
+
}): AiwgProbeCommand;
|
|
14
|
+
|
|
15
|
+
export function probeNotOnPathDetail(): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Resolves the aiwg invocation for doctor's discovery self-probes so they
|
|
2
|
+
// run the current entrypoint instead of failing when aiwg is not on PATH.
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export function resolveAiwgProbeCommand({
|
|
7
|
+
env = process.env,
|
|
8
|
+
argv1 = process.argv[1],
|
|
9
|
+
execPath = process.execPath,
|
|
10
|
+
cwd = process.cwd(),
|
|
11
|
+
packageRoot = null,
|
|
12
|
+
} = {}) {
|
|
13
|
+
const override = env?.AIWG_BIN;
|
|
14
|
+
if (typeof override === 'string' && override.length > 0) {
|
|
15
|
+
return { file: override, prefix: [], via: 'aiwg-bin' };
|
|
16
|
+
}
|
|
17
|
+
const entry = typeof argv1 === 'string' && argv1.length > 0
|
|
18
|
+
? (path.isAbsolute(argv1) ? argv1 : path.resolve(cwd, argv1))
|
|
19
|
+
: null;
|
|
20
|
+
// Only the aiwg launcher itself is a usable entrypoint; doctor also runs as
|
|
21
|
+
// `node tools/cli/doctor.mjs`, where argv[1] is doctor, not aiwg.
|
|
22
|
+
const isLauncher = entry && ['aiwg', 'aiwg.mjs'].includes(path.basename(entry));
|
|
23
|
+
if (entry && isLauncher && existsSync(entry)) {
|
|
24
|
+
return { file: execPath, prefix: [entry], via: 'entrypoint' };
|
|
25
|
+
}
|
|
26
|
+
if (typeof packageRoot === 'string' && packageRoot.length > 0) {
|
|
27
|
+
const bin = path.join(packageRoot, 'bin', 'aiwg.mjs');
|
|
28
|
+
if (existsSync(bin)) {
|
|
29
|
+
return { file: execPath, prefix: [bin], via: 'package' };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { file: 'aiwg', prefix: [], via: 'path' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function probeNotOnPathDetail() {
|
|
36
|
+
return 'aiwg not on PATH (spawn ENOENT) — add the npm global bin to PATH or invoke doctor via the install entrypoint';
|
|
37
|
+
}
|
|
@@ -7,7 +7,7 @@ export interface RuleFrontmatter extends Record<string, unknown> {
|
|
|
7
7
|
summary?: string;
|
|
8
8
|
bound_to: RuleBindings;
|
|
9
9
|
paths?: string[];
|
|
10
|
-
enforcedBy?:
|
|
10
|
+
enforcedBy?: string[];
|
|
11
11
|
}
|
|
12
12
|
export interface RuleDiagnostic { severity: 'error' | 'warn' | 'info'; code: string; message: string }
|
|
13
13
|
export type RuleValidationResult =
|
|
@@ -44,8 +44,6 @@ export function validateRuleFrontmatter(data) {
|
|
|
44
44
|
}
|
|
45
45
|
if (!tagged) diagnostics.push({ severity: 'warn', code: 'missing-activation',
|
|
46
46
|
message: `Missing activation; inferred ${inferRuleActivation(data.enforcement)} delivery (migration fallback); add explicit activation frontmatter.` });
|
|
47
|
-
if (Object.hasOwn(data, 'enforcedBy')) diagnostics.push({ severity: 'info', code: 'reserved-enforcedBy',
|
|
48
|
-
message: 'enforcedBy is opaque reserved metadata (#2839); it has no effect.' });
|
|
49
47
|
const valid = !diagnostics.some(item => item.severity === 'error');
|
|
50
48
|
return {
|
|
51
49
|
valid, diagnostics,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Read rule sources and report declared gate coverage without executing gates. */
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { buildGatesRegistry, installedAiwgRoot } from '../gates/driver.js';
|
|
5
|
+
import { parseRuleFrontmatter } from './activation.mjs';
|
|
6
|
+
import { ruleFiles } from './pin-state.js';
|
|
7
|
+
import { ruleGateReferenceErrors } from './gate-references.js';
|
|
8
|
+
function bundleRuleDirs(root) {
|
|
9
|
+
if (!existsSync(root))
|
|
10
|
+
return [];
|
|
11
|
+
return readdirSync(root, { withFileTypes: true }).filter(entry => entry.isDirectory())
|
|
12
|
+
.map(entry => path.join(root, entry.name, 'rules'));
|
|
13
|
+
}
|
|
14
|
+
export function listRuleGateCoverage(options = {}, ruleId) {
|
|
15
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
16
|
+
const root = path.resolve(options.aiwgRoot ?? installedAiwgRoot());
|
|
17
|
+
const dirs = [path.join(cwd, 'rules'), path.join(cwd, '.aiwg/rules')];
|
|
18
|
+
for (const group of ['frameworks', 'addons', 'extensions']) {
|
|
19
|
+
dirs.push(...bundleRuleDirs(path.join(root, 'agentic/code', group)), ...bundleRuleDirs(path.join(cwd, '.aiwg', group)));
|
|
20
|
+
}
|
|
21
|
+
for (const candidate of options.packDirs ?? []) {
|
|
22
|
+
const dir = path.resolve(cwd, candidate);
|
|
23
|
+
if (statSync(dir).isDirectory())
|
|
24
|
+
dirs.push(path.join(dir, 'rules'));
|
|
25
|
+
}
|
|
26
|
+
const files = [...new Set(dirs.flatMap(ruleFiles))].sort().filter(file => !/^(?:README|RULES-INDEX|RULES-ONDEMAND)\.md$/.test(path.basename(file)) && !file.endsWith('.core.md'));
|
|
27
|
+
const { registry } = buildGatesRegistry(options);
|
|
28
|
+
const rules = [];
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
const name = path.basename(file, '.md');
|
|
31
|
+
const id = path.relative(file.startsWith(`${root}${path.sep}`) ? root : cwd, file).split(path.sep).join('/');
|
|
32
|
+
// Match exact names or source identities; suffix guesses can select another rule.
|
|
33
|
+
if (ruleId && ![name, id, file].includes(ruleId))
|
|
34
|
+
continue;
|
|
35
|
+
const parsed = parseRuleFrontmatter(readFileSync(file, 'utf8'));
|
|
36
|
+
if (!parsed.valid)
|
|
37
|
+
throw new Error(`${file}: ${parsed.diagnostics.map(item => item.message).join('; ')}`);
|
|
38
|
+
const enforcedBy = parsed.rule.enforcedBy ?? [];
|
|
39
|
+
const errors = ruleGateReferenceErrors(enforcedBy, registry);
|
|
40
|
+
if (errors.length)
|
|
41
|
+
throw new Error(`${file}: ${errors.join('; ')}`);
|
|
42
|
+
rules.push({ id, name, file, enforcedBy, status: enforcedBy.length ? 'gate-backed' : 'guidance-only' });
|
|
43
|
+
}
|
|
44
|
+
return rules;
|
|
45
|
+
}
|
|
46
|
+
export function formatRuleGateCoverage(rules) {
|
|
47
|
+
if (!rules.length)
|
|
48
|
+
return 'No rules found.';
|
|
49
|
+
return rules.map(rule => `${rule.id}: ${rule.status}${rule.enforcedBy.length ? `\n${rule.enforcedBy.map(gate => ` ${gate}`).join('\n')}` : ''}`).join('\n');
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=gate-coverage.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function ruleGateReferenceErrors(references, registry) {
|
|
2
|
+
const errors = [];
|
|
3
|
+
for (const reference of references) {
|
|
4
|
+
const [packId, gateId] = reference.split('#');
|
|
5
|
+
try {
|
|
6
|
+
const pack = registry.resolvePack(packId).resolved;
|
|
7
|
+
if (!pack.spec.gates.some(gate => gate.id === gateId))
|
|
8
|
+
throw new Error(`unknown gate '${gateId}' in ${packId}`);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
errors.push(`enforcedBy '${reference}': ${error instanceof Error ? error.message : String(error)}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return errors;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=gate-references.js.map
|
|
@@ -66,7 +66,13 @@
|
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
"enforcedBy": {
|
|
69
|
-
"description": "
|
|
69
|
+
"description": "Gate references as <namespaced-pack-id>#<gate-id>. Lint resolves each reference against the gate registry. Declares coverage without changing activation or executing gates.",
|
|
70
|
+
"type": "array",
|
|
71
|
+
"uniqueItems": true,
|
|
72
|
+
"items": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"pattern": "^(?:(?:aiwg|framework|addon|extension):[^/\\s:#]+/[^/\\s:#]+|project:[^/\\s:#]+)#[^\\s#]+$"
|
|
75
|
+
}
|
|
70
76
|
}
|
|
71
77
|
},
|
|
72
78
|
"allOf": [
|
|
@@ -6,14 +6,18 @@ export class WorkerRequestArgumentError extends WorkerPolicyError {
|
|
|
6
6
|
constructor(key) {
|
|
7
7
|
super('OG_CONFIG_REFERENCE', key === 'task'
|
|
8
8
|
? `Missing required --task. Valid task classes: ${Object.keys(DEFAULT_WORKER_SELECTION.taskClasses).join(', ')}, plus classes configured in defaults.workerSelection.taskClasses.`
|
|
9
|
-
:
|
|
10
|
-
|
|
9
|
+
: key === 'wallSeconds' ? '--timeout-ms must be a positive safe integer multiple of 1000 (whole seconds).'
|
|
10
|
+
: key === 'maxAttempts' || key === 'tokens' ? `--${key === 'tokens' ? 'max-output-tokens' : 'max-attempts'} must be a positive safe integer.`
|
|
11
|
+
: key === 'costUsd' ? '--max-usd must be a finite nonnegative number.'
|
|
12
|
+
: `--${key.replace('_', '-')} must be an absolute path; relative paths are not resolved against cwd or --workdir.`);
|
|
13
|
+
this.pointer = `/request/${['maxAttempts', 'tokens', 'costUsd', 'wallSeconds'].includes(key) ? 'ceilings/' : ''}${key}`;
|
|
11
14
|
}
|
|
12
15
|
}
|
|
13
16
|
/** Reject unsupported syntax before discovery or any path reads. */
|
|
14
17
|
export function parseWorkerCommandArgs(args) {
|
|
15
18
|
const names = { role: 'role', task: 'task', group: 'group', brief: 'brief',
|
|
16
|
-
workdir: 'workdir', 'final-output': 'final_output', transport: 'transport', format: 'format'
|
|
19
|
+
workdir: 'workdir', 'final-output': 'final_output', transport: 'transport', format: 'format',
|
|
20
|
+
'max-attempts': 'maxAttempts', 'timeout-ms': 'wallSeconds', 'max-output-tokens': 'tokens', 'max-usd': 'costUsd' };
|
|
17
21
|
const result = {};
|
|
18
22
|
let json = false;
|
|
19
23
|
let premiumAuthorized = false;
|
|
@@ -63,6 +67,19 @@ export function parseWorkerCommandArgs(args) {
|
|
|
63
67
|
throw new Error('Unsupported transport');
|
|
64
68
|
if (result.format && !['shell', 'json'].includes(result.format))
|
|
65
69
|
throw new Error('Unsupported renderer; use POSIX shell or JSON');
|
|
66
|
-
|
|
70
|
+
const ceilings = {};
|
|
71
|
+
for (const key of ['maxAttempts', 'wallSeconds', 'tokens', 'costUsd']) {
|
|
72
|
+
if (result[key] === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
const value = Number(result[key]);
|
|
75
|
+
if (!result[key].trim() || !Number.isFinite(value) || value < 0
|
|
76
|
+
|| key !== 'costUsd' && (!Number.isSafeInteger(value) || value === 0)
|
|
77
|
+
|| key === 'wallSeconds' && value % 1000 !== 0)
|
|
78
|
+
throw new WorkerRequestArgumentError(key);
|
|
79
|
+
ceilings[key] = key === 'wallSeconds' ? value / 1000 : value;
|
|
80
|
+
delete result[key];
|
|
81
|
+
}
|
|
82
|
+
return { ...result, ...(Object.keys(ceilings).length ? { ceilings } : {}),
|
|
83
|
+
...(premiumAuthorized ? { authority: { premiumAuthorized: true, maxAutoTier: 3 } } : {}) };
|
|
67
84
|
}
|
|
68
85
|
//# sourceMappingURL=worker-command-request.js.map
|
|
@@ -104,7 +104,9 @@ export async function prepareWorkerInvocation(invocation, qualification) {
|
|
|
104
104
|
// The frozen 'closed' value means empty stdin (immediate EOF). 'brief-json'
|
|
105
105
|
// pipes a conductor-built params document framed from the brief file
|
|
106
106
|
// (worker-stdin-frame.mjs), so brief text never enters any argv.
|
|
107
|
-
const
|
|
107
|
+
const deadline = qualification.ceilings?.wallSeconds === undefined ? ''
|
|
108
|
+
: `timeout --signal=KILL ${quote(`${Math.min(result.health?.hardTimeoutSeconds ?? Infinity, result.limits.wallSeconds)}s`)} `;
|
|
109
|
+
const worker = `${deadline}env -i${environment ? ` ${environment}` : ''} ${result.argv.map(quote).join(' ')}`;
|
|
108
110
|
const outputs = `> ${quote(result.stdoutPath)} 2> ${quote(result.stderrPath)}`;
|
|
109
111
|
if (result.stdin === 'brief-json') {
|
|
110
112
|
const framer = fileURLToPath(new URL('./worker-stdin-frame.mjs', import.meta.url));
|
|
@@ -7,6 +7,7 @@ export { inspectWorkerPath } from './worker-invocation-paths.js';
|
|
|
7
7
|
import { createHash, randomUUID } from 'node:crypto';
|
|
8
8
|
import { resolveWorkerPlan } from '../models/resolver.js';
|
|
9
9
|
import { workerConfigDigest, validateWorkerHealthConfig } from '../config/aiwg-config.js';
|
|
10
|
+
import { intersectWorkerCeilings } from '../models/worker-ceilings.js';
|
|
10
11
|
import { assertValidWorkerPayload } from '../mission-protocol/codecs.js';
|
|
11
12
|
import { qualificationDigest } from '../models/qualification-digest.mjs';
|
|
12
13
|
import { prepareWorkerInvocation } from './worker-invocation-preparation.js';
|
|
@@ -180,19 +181,19 @@ export async function resolveWorkerInvocation(snapshot, request, drivers, now =
|
|
|
180
181
|
throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Brief role/task/group/workspace/output differs from request');
|
|
181
182
|
}
|
|
182
183
|
}
|
|
184
|
+
// Brief and parent authority restrict caller ceilings; neither can overwrite them.
|
|
185
|
+
const intent = managedIntent ?? brief?.intent;
|
|
186
|
+
if (intent)
|
|
187
|
+
snapshot.constraints = { ...snapshot.constraints, ceilings: intersectWorkerCeilings(snapshot.constraints?.ceilings, { ...intent.limits, costUsd: intent.limits.costUsd ?? undefined }) };
|
|
183
188
|
plan = resolveWorkerPlan(snapshot, { role: request.role, taskClass: request.task, group: request.group,
|
|
184
189
|
transport: request.transport, authority: request.authority, now, expectedDigests: expected?.digests,
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
} } : {}),
|
|
189
|
-
...(managedIntent ? { dataClass: managedIntent.dataClass, member: managedIntent.pins.member,
|
|
190
|
-
model: managedIntent.pins.model, effort: managedIntent.pins.effort?.value,
|
|
191
|
-
ceilings: { ...managedIntent.limits, costUsd: managedIntent.limits.costUsd ?? undefined } } : {}),
|
|
190
|
+
ceilings: request.ceilings,
|
|
191
|
+
...(intent ? { dataClass: intent.dataClass, member: intent.pins.member,
|
|
192
|
+
model: intent.pins.model, effort: intent.pins.effort?.value } : {}),
|
|
192
193
|
context: { estimator: 'utf8-byte-upper-bound-v1', briefTokens: bytes?.length ?? 0,
|
|
193
194
|
inputTokens: 0, toolOutputAllowance: 1024, reservedOutput: 1024 },
|
|
194
195
|
requiredControls: snapshot.config.parallelism?.health?.requireStepCap ? ['stepCap'] : [],
|
|
195
|
-
});
|
|
196
|
+
}, { tokens: 50000, wallSeconds: 900 });
|
|
196
197
|
plan.request = structuredClone(request);
|
|
197
198
|
if (!plan.selected)
|
|
198
199
|
return plan;
|
|
@@ -246,8 +247,8 @@ export async function resolveWorkerInvocation(snapshot, request, drivers, now =
|
|
|
246
247
|
if (!await inspectPath(path.dirname(output), 'directory'))
|
|
247
248
|
outputParentsExist = false;
|
|
248
249
|
}
|
|
249
|
-
const limits = { tokens:
|
|
250
|
-
wallSeconds:
|
|
250
|
+
const limits = { tokens: plan.ceilings.tokens, costUsd: plan.ceilings.costUsd ?? null,
|
|
251
|
+
wallSeconds: plan.ceilings.wallSeconds, maxAttempts: plan.ceilings.maxAttempts };
|
|
251
252
|
const observation = snapshot.catalog.discovery.instances[tuple.cli].models.find(row => row.id === tuple.model || row.apiModelId === tuple.model).worker;
|
|
252
253
|
const profile = adapter.invocationProfiles[tuple.profile];
|
|
253
254
|
const expiresAt = new Date(Math.min(Date.parse(now) + 300000, Date.parse(tuple.observedAt) + (plan.policy?.freshness?.blockHours ?? DEFAULT_WORKER_SELECTION.freshness.blockHours) * 3600000, Math.min(Date.parse(tuple.verifiedAt), Date.parse(adapter.qualification.verifiedAt), profile.installationQualification ? Date.parse(profile.installationQualification.verifiedAt) : Infinity)
|
|
@@ -290,6 +291,15 @@ export async function resolveWorkerInvocation(snapshot, request, drivers, now =
|
|
|
290
291
|
throw new Error('Installed adapter stdin framer is required');
|
|
291
292
|
if (transport.mcp?.timeoutSeconds !== undefined)
|
|
292
293
|
effectiveHealth = resolveWorkerEffectiveHealth(healthConfig, { ...healthInput, transportSeconds: Math.min(healthInput.transportSeconds ?? Infinity, transport.mcp.timeoutSeconds) });
|
|
294
|
+
if (request.ceilings?.wallSeconds !== undefined && request.ceilings.wallSeconds > effectiveHealth.hardTimeoutSeconds) {
|
|
295
|
+
plan.status = 'blocked';
|
|
296
|
+
plan.selected = null;
|
|
297
|
+
plan.diagnostics.push({ code: 'OG_POLICY_RELAXATION', pointer: '/request/ceilings/wallSeconds',
|
|
298
|
+
message: `Requested wallSeconds ceiling ${request.ceilings.wallSeconds} exceeds resolved policy ceiling ${effectiveHealth.hardTimeoutSeconds}.` });
|
|
299
|
+
return plan;
|
|
300
|
+
}
|
|
301
|
+
if (effectiveHealth.hardTimeoutSeconds === limits.wallSeconds)
|
|
302
|
+
effectiveHealth.origins?.hardTimeoutSeconds.push(...(plan.origins?.['/ceilings/wallSeconds'] ?? []).map(origin => origin.pointer));
|
|
293
303
|
if (workerConfigDigest(driver.qualification) !== qualificationDigest)
|
|
294
304
|
throw new WorkerPolicyError('OG_PIN_MISMATCH', 'Installation profile digest changed during resolution');
|
|
295
305
|
const invocation = { version: 1, mode: managedIntent ? 'managed' : 'direct', transport: tuple.transport, selected,
|
|
@@ -306,6 +316,7 @@ export async function resolveWorkerInvocation(snapshot, request, drivers, now =
|
|
|
306
316
|
controlEvidence: isOperatorQualifiedWorkerAdapter(adapter) ? [adapter.qualification.evidenceRef, ...controls.map(name => profile.controls[name].evidence)] : controls.map(name => profile.controls[name].evidence),
|
|
307
317
|
health: effectiveHealth, };
|
|
308
318
|
const invocationQualification = { ...qualification, now: Date.parse(now),
|
|
319
|
+
ceilings: request.ceilings,
|
|
309
320
|
policy: plan.policy, permissions: tuple.permissions, role: request.role, modelObservation: observation,
|
|
310
321
|
expectedDigests: { policyDigest, catalogDigest: plan.digests.catalogDigest, profileDigest: plan.digests.profileDigest, briefDigest },
|
|
311
322
|
requiredControls: controls };
|
|
@@ -25,6 +25,7 @@ export class DeepSeekHarnessSessionAdapter {
|
|
|
25
25
|
limits;
|
|
26
26
|
maxFiles;
|
|
27
27
|
provider = 'deepseek-harness';
|
|
28
|
+
sourceSchemaMajor = 2;
|
|
28
29
|
adapterVersion = DEEPSEEK_HARNESS_ADAPTER_VERSION;
|
|
29
30
|
disposition = 'implemented';
|
|
30
31
|
supportedOperations = ['discover', 'inspect', 'stream'];
|