@odla-ai/cli 0.24.0 → 0.25.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/dist/bin.cjs +494 -179
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-7P7MI4WX.js → chunk-HG63GLGU.js} +495 -180
- package/dist/chunk-HG63GLGU.js.map +1 -0
- package/dist/index.cjs +494 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-7P7MI4WX.js.map +0 -1
|
@@ -540,23 +540,23 @@ function adminAiAuditQuery(filters) {
|
|
|
540
540
|
}
|
|
541
541
|
return `?limit=${filters.limit}`;
|
|
542
542
|
}
|
|
543
|
-
async function readAdminAiAudit(
|
|
544
|
-
const response2 = await
|
|
545
|
-
headers:
|
|
543
|
+
async function readAdminAiAudit(request2) {
|
|
544
|
+
const response2 = await request2.fetch(`${request2.platform}/registry/platform/ai-audit${request2.query}`, {
|
|
545
|
+
headers: request2.headers
|
|
546
546
|
});
|
|
547
547
|
const body = await responseBody(response2);
|
|
548
548
|
if (!response2.ok) throw new Error(apiError(response2.status, body));
|
|
549
|
-
if (
|
|
550
|
-
|
|
549
|
+
if (request2.json) {
|
|
550
|
+
request2.stdout.log(JSON.stringify(body, null, 2));
|
|
551
551
|
return;
|
|
552
552
|
}
|
|
553
553
|
const events = isRecord(body) && Array.isArray(body.events) ? body.events.filter(isRecord) : [];
|
|
554
|
-
|
|
554
|
+
request2.stdout.log("when change target before -> after actor");
|
|
555
555
|
for (const event of events) {
|
|
556
556
|
const before = isRecord(event.oldPolicy) ? event.oldPolicy : void 0;
|
|
557
557
|
const after = isRecord(event.newPolicy) ? event.newPolicy : void 0;
|
|
558
558
|
const route2 = before && after ? `${String(before.provider)}/${String(before.model)}@v${String(before.version)} -> ${String(after.provider)}/${String(after.model)}@v${String(after.version)}` : "value not retained";
|
|
559
|
-
|
|
559
|
+
request2.stdout.log([
|
|
560
560
|
timestamp(event.createdAt),
|
|
561
561
|
String(event.changeKind ?? ""),
|
|
562
562
|
String(event.purpose ?? event.provider ?? ""),
|
|
@@ -598,14 +598,14 @@ function adminAiUsageQuery(filters) {
|
|
|
598
598
|
const query = params.toString();
|
|
599
599
|
return query ? `?${query}` : "";
|
|
600
600
|
}
|
|
601
|
-
async function readAdminAiUsage(
|
|
602
|
-
const res = await
|
|
603
|
-
headers:
|
|
601
|
+
async function readAdminAiUsage(request2) {
|
|
602
|
+
const res = await request2.fetch(`${request2.platform}/registry/platform/ai-usage${request2.query}`, {
|
|
603
|
+
headers: request2.headers
|
|
604
604
|
});
|
|
605
605
|
const body = await responseBody2(res);
|
|
606
606
|
if (!res.ok) throw new Error(apiError2("read platform AI usage", res.status, body));
|
|
607
|
-
if (
|
|
608
|
-
else printUsage(body,
|
|
607
|
+
if (request2.json) request2.stdout.log(JSON.stringify(body, null, 2));
|
|
608
|
+
else printUsage(body, request2.stdout);
|
|
609
609
|
}
|
|
610
610
|
function usageLimit(value) {
|
|
611
611
|
if (!Number.isSafeInteger(value) || value < 1 || value > 500) {
|
|
@@ -2381,10 +2381,10 @@ function installSkill(options = {}) {
|
|
|
2381
2381
|
current.add(target);
|
|
2382
2382
|
targets.set(harness, current);
|
|
2383
2383
|
};
|
|
2384
|
-
const plan = (target,
|
|
2384
|
+
const plan = (target, content2, managedMerge = false, boundary = root) => {
|
|
2385
2385
|
const existing = plans.get(target);
|
|
2386
|
-
if (existing && existing.content !==
|
|
2387
|
-
plans.set(target, { target, content, boundary, managedMerge });
|
|
2386
|
+
if (existing && existing.content !== content2) throw new Error(`internal agent setup conflict for ${target}`);
|
|
2387
|
+
plans.set(target, { target, content: content2, boundary, managedMerge });
|
|
2388
2388
|
};
|
|
2389
2389
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
2390
2390
|
for (const rel of files) plan(join5(targetDir2, rel), readFileSync5(join5(sourceDir, rel), "utf8"), false, boundary);
|
|
@@ -2825,15 +2825,15 @@ async function connectGitHubSecuritySource(options) {
|
|
|
2825
2825
|
const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
|
|
2826
2826
|
const wait2 = options.wait ?? waitForHostedPoll;
|
|
2827
2827
|
while (true) {
|
|
2828
|
-
const
|
|
2828
|
+
const state2 = await requestHostedSecurityJson(
|
|
2829
2829
|
options,
|
|
2830
2830
|
`/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
|
|
2831
2831
|
{},
|
|
2832
2832
|
"check GitHub connection"
|
|
2833
2833
|
);
|
|
2834
|
-
if (
|
|
2835
|
-
if (
|
|
2836
|
-
throw new Error(`GitHub connection ${
|
|
2834
|
+
if (state2.status !== "pending") {
|
|
2835
|
+
if (state2.status === "connected") return state2;
|
|
2836
|
+
throw new Error(`GitHub connection ${state2.status}${state2.failureCode ? `: ${state2.failureCode}` : ""}`);
|
|
2837
2837
|
}
|
|
2838
2838
|
if (now() >= deadline) throw new Error("GitHub connection approval timed out");
|
|
2839
2839
|
await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
@@ -3388,11 +3388,11 @@ async function gitBlobs(cwd, entries, maxBytes) {
|
|
|
3388
3388
|
if (!Number.isSafeInteger(length) || length < 0 || newline + 1 + length >= output.length) {
|
|
3389
3389
|
throw new Error("git object output has an invalid length");
|
|
3390
3390
|
}
|
|
3391
|
-
const
|
|
3391
|
+
const content2 = output.subarray(newline + 1, newline + 1 + length);
|
|
3392
3392
|
if (output[newline + 1 + length] !== 10) throw new Error("git object output is malformed");
|
|
3393
|
-
total +=
|
|
3393
|
+
total += content2.byteLength;
|
|
3394
3394
|
if (total > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
3395
|
-
blobs.push(
|
|
3395
|
+
blobs.push(content2);
|
|
3396
3396
|
offset = newline + 1 + length + 1;
|
|
3397
3397
|
}
|
|
3398
3398
|
return blobs;
|
|
@@ -3415,13 +3415,13 @@ async function materializeGitTree(source, commitSha, options = {}) {
|
|
|
3415
3415
|
try {
|
|
3416
3416
|
const blobs = await gitBlobs(sourceDir, entries, maxBytes);
|
|
3417
3417
|
for (const [index, entry] of entries.entries()) {
|
|
3418
|
-
const
|
|
3419
|
-
byteCount +=
|
|
3418
|
+
const content2 = blobs[index];
|
|
3419
|
+
byteCount += content2.byteLength;
|
|
3420
3420
|
if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
3421
3421
|
const target = resolve6(targetRoot, entry.path);
|
|
3422
3422
|
if (!target.startsWith(`${resolve6(targetRoot)}${sep2}`)) throw new TypeError("Git tree path escapes workspace");
|
|
3423
3423
|
await mkdir(resolve6(target, ".."), { recursive: true });
|
|
3424
|
-
await writeFile2(target,
|
|
3424
|
+
await writeFile2(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
|
|
3425
3425
|
}
|
|
3426
3426
|
return {
|
|
3427
3427
|
root,
|
|
@@ -3799,11 +3799,11 @@ var ID2 = /^[A-Za-z0-9._:-]{1,180}$/;
|
|
|
3799
3799
|
var MAX_PATCH_BYTES = 256 * 1024;
|
|
3800
3800
|
var MAX_STATE_BYTES = 64 * 1024;
|
|
3801
3801
|
async function createCodePortableCheckpoint(input) {
|
|
3802
|
-
const
|
|
3802
|
+
const state2 = normalizeState(input.state);
|
|
3803
3803
|
validateBaseAndPatch(input.baseCommitSha, input.patch);
|
|
3804
3804
|
const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
|
|
3805
|
-
const stateDigest = `sha256:${await sha256Hex(canonicalJson(
|
|
3806
|
-
const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state, stateDigest };
|
|
3805
|
+
const stateDigest = `sha256:${await sha256Hex(canonicalJson(state2))}`;
|
|
3806
|
+
const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state: state2, stateDigest };
|
|
3807
3807
|
const checkpointDigest = `sha256:${await sha256Hex(canonicalJson(fields))}`;
|
|
3808
3808
|
return freeze({ ...fields, patch: input.patch, checkpointDigest });
|
|
3809
3809
|
}
|
|
@@ -3824,8 +3824,8 @@ async function verifyCodePortableCheckpoint(value) {
|
|
|
3824
3824
|
return created;
|
|
3825
3825
|
}
|
|
3826
3826
|
function normalizeState(value) {
|
|
3827
|
-
const
|
|
3828
|
-
exact2(
|
|
3827
|
+
const state2 = object(value, "state");
|
|
3828
|
+
exact2(state2, [
|
|
3829
3829
|
"planCursor",
|
|
3830
3830
|
"conversationRefs",
|
|
3831
3831
|
"planningInputDigest",
|
|
@@ -3837,13 +3837,13 @@ function normalizeState(value) {
|
|
|
3837
3837
|
"unresolvedApprovals",
|
|
3838
3838
|
"trustStatus"
|
|
3839
3839
|
]);
|
|
3840
|
-
const planCursor =
|
|
3841
|
-
const conversations = strings(
|
|
3842
|
-
const approvals = strings(
|
|
3843
|
-
if (planCursor !== null && (typeof planCursor !== "string" || !ID2.test(planCursor)) || typeof
|
|
3840
|
+
const planCursor = state2.planCursor;
|
|
3841
|
+
const conversations = strings(state2.conversationRefs, "conversationRefs", ID2, 256, false);
|
|
3842
|
+
const approvals = strings(state2.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
|
|
3843
|
+
if (planCursor !== null && (typeof planCursor !== "string" || !ID2.test(planCursor)) || typeof state2.planningInputDigest !== "string" || !DIGEST2.test(state2.planningInputDigest) || typeof state2.buildPolicyDigest !== "string" || !DIGEST2.test(state2.buildPolicyDigest) || state2.dependencyLayerDigest !== null && (typeof state2.dependencyLayerDigest !== "string" || !DIGEST2.test(state2.dependencyLayerDigest)) || state2.verificationDigest !== null && (typeof state2.verificationDigest !== "string" || !DIGEST2.test(state2.verificationDigest)) || state2.reviewDigest !== null && (typeof state2.reviewDigest !== "string" || !DIGEST2.test(state2.reviewDigest)) || !["candidate_untrusted", "verified", "reviewed"].includes(String(state2.trustStatus)) || !Array.isArray(state2.completedEffects) || state2.completedEffects.length > 256) {
|
|
3844
3844
|
throw invalid2("Portable checkpoint state is malformed or outside its bounds.");
|
|
3845
3845
|
}
|
|
3846
|
-
const effects =
|
|
3846
|
+
const effects = state2.completedEffects.map((item) => {
|
|
3847
3847
|
const effect = object(item, "completed effect");
|
|
3848
3848
|
exact2(effect, ["effectId", "actionDigest", "receiptDigest"]);
|
|
3849
3849
|
if (typeof effect.effectId !== "string" || !ID2.test(effect.effectId) || typeof effect.actionDigest !== "string" || !DIGEST2.test(effect.actionDigest) || typeof effect.receiptDigest !== "string" || !DIGEST2.test(effect.receiptDigest)) {
|
|
@@ -3858,18 +3858,18 @@ function normalizeState(value) {
|
|
|
3858
3858
|
if (new Set(effects.map((item) => item.effectId)).size !== effects.length) {
|
|
3859
3859
|
throw invalid2("Portable checkpoint repeats a completed effect.");
|
|
3860
3860
|
}
|
|
3861
|
-
const trustStatus =
|
|
3862
|
-
if (trustStatus === "candidate_untrusted" && (
|
|
3861
|
+
const trustStatus = state2.trustStatus;
|
|
3862
|
+
if (trustStatus === "candidate_untrusted" && (state2.verificationDigest !== null || state2.reviewDigest !== null) || trustStatus === "verified" && (state2.verificationDigest === null || state2.reviewDigest !== null) || trustStatus === "reviewed" && (state2.verificationDigest === null || state2.reviewDigest === null)) {
|
|
3863
3863
|
throw invalid2("Portable checkpoint trust status does not match its evidence digests.");
|
|
3864
3864
|
}
|
|
3865
3865
|
const normalized = {
|
|
3866
3866
|
planCursor,
|
|
3867
3867
|
conversationRefs: conversations,
|
|
3868
|
-
planningInputDigest:
|
|
3869
|
-
buildPolicyDigest:
|
|
3870
|
-
dependencyLayerDigest:
|
|
3871
|
-
verificationDigest:
|
|
3872
|
-
reviewDigest:
|
|
3868
|
+
planningInputDigest: state2.planningInputDigest,
|
|
3869
|
+
buildPolicyDigest: state2.buildPolicyDigest,
|
|
3870
|
+
dependencyLayerDigest: state2.dependencyLayerDigest,
|
|
3871
|
+
verificationDigest: state2.verificationDigest,
|
|
3872
|
+
reviewDigest: state2.reviewDigest,
|
|
3873
3873
|
completedEffects: effects,
|
|
3874
3874
|
unresolvedApprovals: approvals,
|
|
3875
3875
|
trustStatus
|
|
@@ -3986,60 +3986,60 @@ async function createConversionRegistry(config) {
|
|
|
3986
3986
|
policies.set(policy.conversionId, Object.freeze(policy));
|
|
3987
3987
|
}
|
|
3988
3988
|
const outputCounts = /* @__PURE__ */ new Map();
|
|
3989
|
-
const
|
|
3989
|
+
const get2 = (id, kind) => {
|
|
3990
3990
|
const policy = policies.get(id);
|
|
3991
3991
|
if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
|
|
3992
3992
|
return policy;
|
|
3993
3993
|
};
|
|
3994
3994
|
const checked = (source, id, kind) => {
|
|
3995
|
-
const policy =
|
|
3995
|
+
const policy = get2(id, kind);
|
|
3996
3996
|
if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
|
|
3997
3997
|
return policy;
|
|
3998
3998
|
};
|
|
3999
|
-
const
|
|
3999
|
+
const emit3 = (source, policy, value) => convert(source, policy, value, outputCounts);
|
|
4000
4000
|
const operations = Object.freeze({
|
|
4001
4001
|
boolean: async (value, id) => {
|
|
4002
4002
|
const policy = checked(value, id, "boolean");
|
|
4003
|
-
return
|
|
4003
|
+
return emit3(value, policy, requireBoolean(value.value));
|
|
4004
4004
|
},
|
|
4005
4005
|
integer: async (value, id) => {
|
|
4006
4006
|
const policy = checked(value, id, "integer");
|
|
4007
|
-
return
|
|
4007
|
+
return emit3(value, policy, boundedInteger(value.value, policy.output));
|
|
4008
4008
|
},
|
|
4009
4009
|
finiteNumber: async (value, id) => {
|
|
4010
4010
|
const policy = checked(value, id, "finite_number");
|
|
4011
|
-
return
|
|
4011
|
+
return emit3(value, policy, boundedNumber(value.value, policy.output));
|
|
4012
4012
|
},
|
|
4013
4013
|
enum: async (value, id) => {
|
|
4014
4014
|
const policy = checked(value, id, "enum");
|
|
4015
|
-
return
|
|
4015
|
+
return emit3(value, policy, enumMember(value.value, policy.output));
|
|
4016
4016
|
},
|
|
4017
4017
|
date: async (value, id) => {
|
|
4018
4018
|
const policy = checked(value, id, "date");
|
|
4019
|
-
return
|
|
4019
|
+
return emit3(value, policy, canonicalDate(value.value, policy.output));
|
|
4020
4020
|
},
|
|
4021
4021
|
registeredId: async (value, id) => {
|
|
4022
4022
|
const policy = checked(value, id, "registered_id");
|
|
4023
4023
|
const registry = config.registeredIds?.[policy.output.registryId];
|
|
4024
4024
|
const output = typeof value.value === "string" ? registry?.values[value.value] : void 0;
|
|
4025
4025
|
if (!output) throw new CamelError("conversion_rejected", "Registered-ID conversion rejected the candidate.");
|
|
4026
|
-
return
|
|
4026
|
+
return emit3(value, policy, output);
|
|
4027
4027
|
},
|
|
4028
4028
|
digest: async (value, id) => {
|
|
4029
4029
|
const policy = checked(value, id, "digest");
|
|
4030
4030
|
if (!(value.value instanceof Uint8Array)) throw new CamelError("conversion_rejected", "Digest conversion requires bytes.");
|
|
4031
|
-
return
|
|
4031
|
+
return emit3(value, policy, await sha256Hex(value.value));
|
|
4032
4032
|
},
|
|
4033
4033
|
measure: async (value, metric, id) => {
|
|
4034
4034
|
const policy = checked(value, id, "integer");
|
|
4035
4035
|
const measured = measure(value.value, metric);
|
|
4036
|
-
return
|
|
4036
|
+
return emit3(value, policy, boundedInteger(measured, policy.output));
|
|
4037
4037
|
},
|
|
4038
4038
|
test: async (value, predicateId, id) => {
|
|
4039
4039
|
const policy = checked(value, id, "boolean");
|
|
4040
4040
|
const predicate = config.predicates?.[predicateId];
|
|
4041
4041
|
if (!predicate) throw new CamelError("conversion_rejected", "Predicate is not registered.");
|
|
4042
|
-
return
|
|
4042
|
+
return emit3(value, policy, evaluatePredicate(value.value, predicate, config.registeredIds));
|
|
4043
4043
|
}
|
|
4044
4044
|
});
|
|
4045
4045
|
return Object.freeze({ operations, policy: (id) => policies.get(id) ?? missingPolicy() });
|
|
@@ -4258,11 +4258,11 @@ async function digestStagedWorkspace(root, limits) {
|
|
|
4258
4258
|
const hash = createHash2("sha256");
|
|
4259
4259
|
let bytes = 0;
|
|
4260
4260
|
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
4261
|
-
const
|
|
4262
|
-
bytes += Buffer.byteLength(file.path) +
|
|
4261
|
+
const content2 = await readFile2(file.target);
|
|
4262
|
+
bytes += Buffer.byteLength(file.path) + content2.byteLength;
|
|
4263
4263
|
if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
|
|
4264
|
-
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${
|
|
4265
|
-
hash.update(
|
|
4264
|
+
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content2.byteLength}:`);
|
|
4265
|
+
hash.update(content2);
|
|
4266
4266
|
}
|
|
4267
4267
|
return `sha256:${hash.digest("hex")}`;
|
|
4268
4268
|
}
|
|
@@ -4357,13 +4357,13 @@ function createCodeRuntimeControlClient(options) {
|
|
|
4357
4357
|
if (!Number.isSafeInteger(modelRequestTimeoutMs) || modelRequestTimeoutMs < 3e4 || modelRequestTimeoutMs > 30 * 6e4) {
|
|
4358
4358
|
throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
|
|
4359
4359
|
}
|
|
4360
|
-
const
|
|
4360
|
+
const request2 = options.fetch ?? fetch;
|
|
4361
4361
|
const call2 = async (path, body, timeoutMs = requestTimeoutMs) => {
|
|
4362
4362
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
4363
4363
|
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
4364
4364
|
let response2;
|
|
4365
4365
|
try {
|
|
4366
|
-
response2 = await
|
|
4366
|
+
response2 = await request2(`${endpoint}${path}`, {
|
|
4367
4367
|
method: "POST",
|
|
4368
4368
|
headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json" },
|
|
4369
4369
|
body: JSON.stringify(body),
|
|
@@ -5270,34 +5270,34 @@ function createCodeToolBroker(options) {
|
|
|
5270
5270
|
const policy = createCodePolicyGate(options);
|
|
5271
5271
|
let tail = Promise.resolve();
|
|
5272
5272
|
return {
|
|
5273
|
-
execute(context,
|
|
5274
|
-
const result = tail.then(() => route(context,
|
|
5273
|
+
execute(context, request2) {
|
|
5274
|
+
const result = tail.then(() => route(context, request2, options, recipes, policy));
|
|
5275
5275
|
tail = result.then(() => void 0, () => void 0);
|
|
5276
5276
|
return result;
|
|
5277
5277
|
}
|
|
5278
5278
|
};
|
|
5279
5279
|
}
|
|
5280
|
-
async function route(context,
|
|
5280
|
+
async function route(context, request2, options, recipes, policy) {
|
|
5281
5281
|
try {
|
|
5282
5282
|
if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
|
|
5283
|
-
if (
|
|
5284
|
-
if (
|
|
5285
|
-
return await recipe(context,
|
|
5283
|
+
if (request2.tool === "sandbox.read") return await read(context, request2, options, policy);
|
|
5284
|
+
if (request2.tool === "sandbox.apply_patch") return await patch(context, request2, options, policy);
|
|
5285
|
+
return await recipe(context, request2, options, recipes, policy);
|
|
5286
5286
|
} catch (reason) {
|
|
5287
|
-
return response(
|
|
5287
|
+
return response(request2, false, reason instanceof TypeError ? reason.message : "tool failed closed");
|
|
5288
5288
|
}
|
|
5289
5289
|
}
|
|
5290
|
-
async function read(context,
|
|
5291
|
-
exactKeys(
|
|
5292
|
-
const path = stringField(
|
|
5293
|
-
const startLine = optionalInteger(
|
|
5294
|
-
const endLine = optionalInteger(
|
|
5290
|
+
async function read(context, request2, options, policy) {
|
|
5291
|
+
exactKeys(request2.input, ["path", "startLine", "endLine"]);
|
|
5292
|
+
const path = stringField(request2.input, "path");
|
|
5293
|
+
const startLine = optionalInteger(request2.input.startLine) ?? 1;
|
|
5294
|
+
const endLine = optionalInteger(request2.input.endLine) ?? startLine + (options.maxReadLines ?? 2e3) - 1;
|
|
5295
5295
|
if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
|
|
5296
5296
|
throw new TypeError("requested line range exceeds its bound");
|
|
5297
5297
|
}
|
|
5298
5298
|
const paths = await registeredFiles(context.workspaceDir, 2e4);
|
|
5299
|
-
const allowed = await policy.read(policyContext(context,
|
|
5300
|
-
if (!allowed) return response(
|
|
5299
|
+
const allowed = await policy.read(policyContext(context, request2, options, { paths, path, startLine, endLine }));
|
|
5300
|
+
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
5301
5301
|
const target = resolveCodePath(context.workspaceDir, path);
|
|
5302
5302
|
const info = await stat2(target);
|
|
5303
5303
|
if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
|
|
@@ -5306,40 +5306,40 @@ async function read(context, request, options, policy) {
|
|
|
5306
5306
|
const source = await readFile22(target);
|
|
5307
5307
|
if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
|
|
5308
5308
|
const lines = source.toString("utf8").split("\n");
|
|
5309
|
-
const
|
|
5310
|
-
if (Buffer.byteLength(
|
|
5309
|
+
const content2 = lines.slice(startLine - 1, endLine).join("\n");
|
|
5310
|
+
if (Buffer.byteLength(content2) > (options.maxReadBytes ?? 128 * 1024)) {
|
|
5311
5311
|
throw new TypeError("read result exceeds its byte bound");
|
|
5312
5312
|
}
|
|
5313
|
-
return response(
|
|
5313
|
+
return response(request2, true, content2, { path, startLine, endLine: Math.min(endLine, lines.length) });
|
|
5314
5314
|
}
|
|
5315
|
-
async function patch(context,
|
|
5316
|
-
exactKeys(
|
|
5317
|
-
const value = stringField(
|
|
5315
|
+
async function patch(context, request2, options, policy) {
|
|
5316
|
+
exactKeys(request2.input, ["patch"]);
|
|
5317
|
+
const value = stringField(request2.input, "patch");
|
|
5318
5318
|
const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
|
|
5319
5319
|
if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
|
|
5320
5320
|
throw new TypeError("patch targets a read-only reference source");
|
|
5321
5321
|
}
|
|
5322
|
-
const allowed = await policy.patch(policyContext(context,
|
|
5323
|
-
if (!allowed) return response(
|
|
5322
|
+
const allowed = await policy.patch(policyContext(context, request2, options, { patch: value }));
|
|
5323
|
+
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
5324
5324
|
await applyCodePatch(context.workspaceDir, value, paths);
|
|
5325
|
-
return response(
|
|
5325
|
+
return response(request2, true, `Applied patch to ${paths.length} file(s).`, { paths });
|
|
5326
5326
|
}
|
|
5327
|
-
async function recipe(context,
|
|
5328
|
-
exactKeys(
|
|
5329
|
-
const recipeId = stringField(
|
|
5327
|
+
async function recipe(context, request2, options, recipes, policy) {
|
|
5328
|
+
exactKeys(request2.input, ["recipeId"]);
|
|
5329
|
+
const recipeId = stringField(request2.input, "recipeId");
|
|
5330
5330
|
const digestLimits = {
|
|
5331
5331
|
maxFiles: options.maxRecipeWorkspaceFiles ?? 2e4,
|
|
5332
5332
|
maxBytes: options.maxRecipeWorkspaceBytes ?? 512 * 1024 * 1024
|
|
5333
5333
|
};
|
|
5334
5334
|
const sourceDigest = await digestStagedWorkspace(context.workspaceDir, digestLimits);
|
|
5335
|
-
const allowed = await policy.recipe(policyContext(context,
|
|
5335
|
+
const allowed = await policy.recipe(policyContext(context, request2, options, {
|
|
5336
5336
|
recipeIds: [...recipes.keys()].sort(),
|
|
5337
5337
|
recipeId,
|
|
5338
5338
|
sourceDigest
|
|
5339
5339
|
}));
|
|
5340
|
-
if (!allowed) return response(
|
|
5340
|
+
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
5341
5341
|
const selected = recipes.get(recipeId);
|
|
5342
|
-
if (!selected) return response(
|
|
5342
|
+
if (!selected) return response(request2, false, "build recipe is not registered");
|
|
5343
5343
|
const staged = await stageWorkspace(context.workspaceDir, {
|
|
5344
5344
|
maxFiles: digestLimits.maxFiles,
|
|
5345
5345
|
maxBytes: digestLimits.maxBytes
|
|
@@ -5356,7 +5356,7 @@ async function recipe(context, request, options, recipes, policy) {
|
|
|
5356
5356
|
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
5357
5357
|
const ok = result.exitCode === 0 && !result.outputLimitExceeded && !result.timedOut;
|
|
5358
5358
|
const status = result.timedOut ? "timed out" : result.outputLimitExceeded ? "exceeded output limit" : ok ? "passed" : `failed with exit ${result.exitCode}`;
|
|
5359
|
-
return response(
|
|
5359
|
+
return response(request2, ok, `Recipe ${recipeId} ${status}.${output ? `
|
|
5360
5360
|
${output}` : ""}`, {
|
|
5361
5361
|
recipeId,
|
|
5362
5362
|
exitCode: result.exitCode,
|
|
@@ -5368,10 +5368,10 @@ ${output}` : ""}`, {
|
|
|
5368
5368
|
await staged.cleanup();
|
|
5369
5369
|
}
|
|
5370
5370
|
}
|
|
5371
|
-
function policyContext(context,
|
|
5371
|
+
function policyContext(context, request2, options, extra) {
|
|
5372
5372
|
return {
|
|
5373
5373
|
lease: context.lease,
|
|
5374
|
-
request,
|
|
5374
|
+
request: request2,
|
|
5375
5375
|
workspaceId: `workspace:${context.lease.task.attemptId}`,
|
|
5376
5376
|
readers: { kind: "principals", principalIds: [options.readerId] },
|
|
5377
5377
|
...extra
|
|
@@ -5421,8 +5421,8 @@ function optionalInteger(value) {
|
|
|
5421
5421
|
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("line bounds must be positive integers");
|
|
5422
5422
|
return value;
|
|
5423
5423
|
}
|
|
5424
|
-
function response(
|
|
5425
|
-
return { requestId:
|
|
5424
|
+
function response(request2, ok, content2, details) {
|
|
5425
|
+
return { requestId: request2.requestId, ok, content: content2, ...details ? { details } : {} };
|
|
5426
5426
|
}
|
|
5427
5427
|
function codeCommandMetadata(payload, resume) {
|
|
5428
5428
|
const trusted = record22(payload.trustedBase);
|
|
@@ -5522,13 +5522,13 @@ function createCodeRuntimeToolBroker(input, lease, role) {
|
|
|
5522
5522
|
readerId: `code-session:${lease.task.taskId}`,
|
|
5523
5523
|
readOnlyPrefixes: [".odla-references"]
|
|
5524
5524
|
});
|
|
5525
|
-
return role === "coding" ? broker : { execute: (context,
|
|
5525
|
+
return role === "coding" ? broker : { execute: (context, request2) => request2.tool === "sandbox.read" ? broker.execute(context, request2) : Promise.resolve({ requestId: request2.requestId, ok: false, content: "review sessions are read-only" }) };
|
|
5526
5526
|
}
|
|
5527
5527
|
async function handleCodeRuntimeInference(input) {
|
|
5528
|
-
const { command, metadata: metadata2, request, state } = input;
|
|
5529
|
-
if (
|
|
5530
|
-
if (!
|
|
5531
|
-
|
|
5528
|
+
const { command, metadata: metadata2, request: request2, state: state2 } = input;
|
|
5529
|
+
if (state2.tokens >= metadata2.maxTokensPerInteraction) {
|
|
5530
|
+
if (!state2.noticeEmitted) {
|
|
5531
|
+
state2.noticeEmitted = true;
|
|
5532
5532
|
await input.event({
|
|
5533
5533
|
type: "message",
|
|
5534
5534
|
actor: "system",
|
|
@@ -5538,7 +5538,7 @@ async function handleCodeRuntimeInference(input) {
|
|
|
5538
5538
|
return {
|
|
5539
5539
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
5540
5540
|
type: "inference.response",
|
|
5541
|
-
requestId:
|
|
5541
|
+
requestId: request2.requestId,
|
|
5542
5542
|
response: {
|
|
5543
5543
|
id: `budget:${command.commandId}`,
|
|
5544
5544
|
provider: "openai",
|
|
@@ -5552,11 +5552,11 @@ async function handleCodeRuntimeInference(input) {
|
|
|
5552
5552
|
}
|
|
5553
5553
|
const startedAt = Date.now();
|
|
5554
5554
|
const response2 = await input.control.infer(command.sessionId, {
|
|
5555
|
-
requestId:
|
|
5555
|
+
requestId: request2.requestId,
|
|
5556
5556
|
interactionId: command.commandId,
|
|
5557
|
-
call:
|
|
5557
|
+
call: request2.call
|
|
5558
5558
|
});
|
|
5559
|
-
|
|
5559
|
+
state2.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
|
|
5560
5560
|
await input.event({
|
|
5561
5561
|
type: "usage",
|
|
5562
5562
|
provider: response2.receipt.provider,
|
|
@@ -5565,13 +5565,13 @@ async function handleCodeRuntimeInference(input) {
|
|
|
5565
5565
|
outputTokens: response2.receipt.outputTokens,
|
|
5566
5566
|
durationMs: Date.now() - startedAt,
|
|
5567
5567
|
interactionId: command.commandId,
|
|
5568
|
-
interactionTokens:
|
|
5568
|
+
interactionTokens: state2.tokens,
|
|
5569
5569
|
interactionMaxTokens: metadata2.maxTokensPerInteraction
|
|
5570
5570
|
}).catch(() => void 0);
|
|
5571
5571
|
return {
|
|
5572
5572
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
5573
5573
|
type: "inference.response",
|
|
5574
|
-
requestId:
|
|
5574
|
+
requestId: request2.requestId,
|
|
5575
5575
|
response: response2.response
|
|
5576
5576
|
};
|
|
5577
5577
|
}
|
|
@@ -5939,6 +5939,14 @@ Usage:
|
|
|
5939
5939
|
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
5940
5940
|
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
5941
5941
|
odla-ai pm handoff --app <id> [--json]
|
|
5942
|
+
odla-ai discuss groups [--json]
|
|
5943
|
+
odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
|
|
5944
|
+
odla-ai discuss read <topic> [--json]
|
|
5945
|
+
odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"]
|
|
5946
|
+
odla-ai discuss reply <topic> --body "..." [--markup "..."]
|
|
5947
|
+
odla-ai discuss resolve <topic> [--reopen]
|
|
5948
|
+
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
5949
|
+
odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
|
|
5942
5950
|
odla-ai whoami [--json]
|
|
5943
5951
|
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
5944
5952
|
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
@@ -6043,6 +6051,13 @@ Commands:
|
|
|
6043
6051
|
"app". Entities: goal (alias conformance), task (alias kanban),
|
|
6044
6052
|
decision, bug. Status changes and comments post to each item's
|
|
6045
6053
|
@odla-ai/chat discussion thread.
|
|
6054
|
+
discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
|
|
6055
|
+
one group per project, topics with replies, @-mentions of people,
|
|
6056
|
+
agents, PM items, and projects. Built for unattended use \u2014 post a
|
|
6057
|
+
question, then "watch" it until someone answers. "watch" exits 75
|
|
6058
|
+
(not 1) when it times out with nothing new, so a script can tell
|
|
6059
|
+
"no answer yet" from a failure and just rerun. Use "who" to look
|
|
6060
|
+
up the exact @[Label](kind/id) markup for a mention.
|
|
6046
6061
|
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
6047
6062
|
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
6048
6063
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
@@ -6782,6 +6797,18 @@ var COMMAND_SURFACE = {
|
|
|
6782
6797
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
6783
6798
|
capabilities: {},
|
|
6784
6799
|
code: { connect: {} },
|
|
6800
|
+
// `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
|
|
6801
|
+
discuss: {
|
|
6802
|
+
groups: {},
|
|
6803
|
+
list: {},
|
|
6804
|
+
topics: {},
|
|
6805
|
+
read: {},
|
|
6806
|
+
post: {},
|
|
6807
|
+
reply: {},
|
|
6808
|
+
resolve: {},
|
|
6809
|
+
who: {},
|
|
6810
|
+
watch: {}
|
|
6811
|
+
},
|
|
6785
6812
|
doctor: {},
|
|
6786
6813
|
help: {},
|
|
6787
6814
|
init: {},
|
|
@@ -6931,17 +6958,17 @@ async function runHostedSecurity(options) {
|
|
|
6931
6958
|
allowNetwork: false
|
|
6932
6959
|
}
|
|
6933
6960
|
});
|
|
6934
|
-
const
|
|
6935
|
-
await writeSecurityArtifacts(output,
|
|
6936
|
-
const reportDigest = await securityFingerprint(
|
|
6961
|
+
const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
6962
|
+
await writeSecurityArtifacts(output, report4);
|
|
6963
|
+
const reportDigest = await securityFingerprint(report4);
|
|
6937
6964
|
await hosted.complete({
|
|
6938
6965
|
reportDigest,
|
|
6939
|
-
coverageStatus:
|
|
6940
|
-
confirmed:
|
|
6941
|
-
candidates:
|
|
6966
|
+
coverageStatus: report4.coverageStatus,
|
|
6967
|
+
confirmed: report4.metrics.confirmed,
|
|
6968
|
+
candidates: report4.metrics.candidates
|
|
6942
6969
|
}, { signal: options.signal });
|
|
6943
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
6944
|
-
return Object.freeze({ report:
|
|
6970
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
|
|
6971
|
+
return Object.freeze({ report: report4, run: hosted.run, output });
|
|
6945
6972
|
}
|
|
6946
6973
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
6947
6974
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -6951,11 +6978,11 @@ function selectEnv(requested, declared, configPath, rootDir) {
|
|
|
6951
6978
|
}
|
|
6952
6979
|
return env;
|
|
6953
6980
|
}
|
|
6954
|
-
async function injectedToken(options,
|
|
6955
|
-
const value = options.token ?? await options.getToken?.(Object.freeze({ ...
|
|
6981
|
+
async function injectedToken(options, request2) {
|
|
6982
|
+
const value = options.token ?? await options.getToken?.(Object.freeze({ ...request2 }));
|
|
6956
6983
|
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
6957
6984
|
throw new Error(
|
|
6958
|
-
|
|
6985
|
+
request2.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
|
|
6959
6986
|
);
|
|
6960
6987
|
}
|
|
6961
6988
|
return value;
|
|
@@ -6967,14 +6994,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
6967
6994
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
6968
6995
|
return { ...profile, maxHuntTasks };
|
|
6969
6996
|
}
|
|
6970
|
-
function printSummary(out, appId, env, run,
|
|
6971
|
-
const complete =
|
|
6997
|
+
function printSummary(out, appId, env, run, report4, output) {
|
|
6998
|
+
const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
|
|
6972
6999
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
6973
7000
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
6974
7001
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
6975
|
-
out.log(` coverage: ${
|
|
6976
|
-
if (
|
|
6977
|
-
out.log(` findings: confirmed=${
|
|
7002
|
+
out.log(` coverage: ${report4.coverageStatus} ${complete}/${report4.coverage.length} blocked=${report4.metrics.blockedCells} shallow=${report4.metrics.shallowCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
|
|
7003
|
+
if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
|
|
7004
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
|
|
6978
7005
|
out.log(` report: ${resolve9(output, "REPORT.md")}`);
|
|
6979
7006
|
}
|
|
6980
7007
|
function formatBudget(usage) {
|
|
@@ -7978,6 +8005,289 @@ async function codeCommand(parsed, dependencies) {
|
|
|
7978
8005
|
});
|
|
7979
8006
|
}
|
|
7980
8007
|
|
|
8008
|
+
// src/discuss-actions.ts
|
|
8009
|
+
async function request(ctx, method, path, body) {
|
|
8010
|
+
const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
|
|
8011
|
+
method,
|
|
8012
|
+
headers: { authorization: `Bearer ${ctx.token}`, "content-type": "application/json" },
|
|
8013
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
8014
|
+
});
|
|
8015
|
+
const data = await res.json().catch(() => ({}));
|
|
8016
|
+
if (!res.ok)
|
|
8017
|
+
throw new Error(`discuss ${method} ${path} failed: ${data.error ?? `registry returned ${res.status}`}`);
|
|
8018
|
+
return data;
|
|
8019
|
+
}
|
|
8020
|
+
function emit(ctx, value, human) {
|
|
8021
|
+
if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
|
|
8022
|
+
else human();
|
|
8023
|
+
}
|
|
8024
|
+
var state = (topic) => topic.resolvedAt ? "resolved" : "open";
|
|
8025
|
+
function bodyWithRefs(post) {
|
|
8026
|
+
if (!post.refs || post.refs.length === 0) return post.body;
|
|
8027
|
+
let out = "";
|
|
8028
|
+
let cursor = 0;
|
|
8029
|
+
for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
|
|
8030
|
+
if (ref.start < cursor || ref.end > post.body.length) continue;
|
|
8031
|
+
out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
|
|
8032
|
+
cursor = ref.end;
|
|
8033
|
+
}
|
|
8034
|
+
return out + post.body.slice(cursor);
|
|
8035
|
+
}
|
|
8036
|
+
function content(parsed) {
|
|
8037
|
+
const markup = stringOpt(parsed.options.markup);
|
|
8038
|
+
if (markup) return { markup };
|
|
8039
|
+
const body = stringOpt(parsed.options.body);
|
|
8040
|
+
if (!body) throw new Error('needs --body "\u2026" (or --markup with @[Label](kind/id) mentions)');
|
|
8041
|
+
return { body };
|
|
8042
|
+
}
|
|
8043
|
+
async function discussGroups(ctx) {
|
|
8044
|
+
const result = await request(
|
|
8045
|
+
ctx,
|
|
8046
|
+
"GET",
|
|
8047
|
+
"/groups"
|
|
8048
|
+
);
|
|
8049
|
+
emit(ctx, result, () => {
|
|
8050
|
+
ctx.out.log("app name started");
|
|
8051
|
+
for (const group of result.groups) {
|
|
8052
|
+
ctx.out.log(`${group.appId} ${group.name} ${group.channelId ? "yes" : "no"}`);
|
|
8053
|
+
}
|
|
8054
|
+
});
|
|
8055
|
+
}
|
|
8056
|
+
async function discussList(ctx, parsed) {
|
|
8057
|
+
const query = new URLSearchParams();
|
|
8058
|
+
for (const [flag, param] of [
|
|
8059
|
+
["app", "app"],
|
|
8060
|
+
["q", "q"],
|
|
8061
|
+
["state", "state"],
|
|
8062
|
+
["limit", "limit"],
|
|
8063
|
+
["offset", "offset"]
|
|
8064
|
+
]) {
|
|
8065
|
+
const value = stringOpt(parsed.options[flag]);
|
|
8066
|
+
if (value) query.set(param, value);
|
|
8067
|
+
}
|
|
8068
|
+
const qs = query.toString();
|
|
8069
|
+
const page = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
|
|
8070
|
+
emit(ctx, page, () => {
|
|
8071
|
+
ctx.out.log(`topics \u2014 ${page.topics.length} of ${page.total}`);
|
|
8072
|
+
ctx.out.log("id state app replies subject");
|
|
8073
|
+
for (const topic of page.topics) {
|
|
8074
|
+
ctx.out.log(
|
|
8075
|
+
`${topic.id} ${state(topic)} ${topic.appId ?? ""} ${topic.replyCount} ${topic.subject}`
|
|
8076
|
+
);
|
|
8077
|
+
}
|
|
8078
|
+
});
|
|
8079
|
+
}
|
|
8080
|
+
async function discussRead(ctx, id) {
|
|
8081
|
+
const result = await request(
|
|
8082
|
+
ctx,
|
|
8083
|
+
"GET",
|
|
8084
|
+
`/topics/${encodeURIComponent(id)}`
|
|
8085
|
+
);
|
|
8086
|
+
emit(ctx, result, () => {
|
|
8087
|
+
ctx.out.log(`${result.topic.subject} [${state(result.topic)}] ${result.topic.appId ?? ""}`);
|
|
8088
|
+
for (const post of result.posts) {
|
|
8089
|
+
const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
|
|
8090
|
+
ctx.out.log(`
|
|
8091
|
+
\u2014 ${who}`);
|
|
8092
|
+
ctx.out.log(bodyWithRefs(post));
|
|
8093
|
+
for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
|
|
8094
|
+
}
|
|
8095
|
+
});
|
|
8096
|
+
}
|
|
8097
|
+
async function discussPost(ctx, parsed) {
|
|
8098
|
+
const appId = stringOpt(parsed.options.app);
|
|
8099
|
+
if (!appId) throw new Error("discuss post needs --app <appId>");
|
|
8100
|
+
const subject = stringOpt(parsed.options.subject);
|
|
8101
|
+
if (!subject) throw new Error('discuss post needs --subject "\u2026"');
|
|
8102
|
+
const created = await request(ctx, "POST", "/topics", {
|
|
8103
|
+
appId,
|
|
8104
|
+
subject,
|
|
8105
|
+
...content(parsed)
|
|
8106
|
+
});
|
|
8107
|
+
emit(ctx, created, () => ctx.out.log(`opened topic ${created.id}`));
|
|
8108
|
+
}
|
|
8109
|
+
async function discussReply(ctx, id, parsed) {
|
|
8110
|
+
const created = await request(
|
|
8111
|
+
ctx,
|
|
8112
|
+
"POST",
|
|
8113
|
+
`/topics/${encodeURIComponent(id)}/replies`,
|
|
8114
|
+
content(parsed)
|
|
8115
|
+
);
|
|
8116
|
+
emit(ctx, created, () => ctx.out.log(`replied ${created.id}`));
|
|
8117
|
+
}
|
|
8118
|
+
async function discussResolve(ctx, id, resolved) {
|
|
8119
|
+
const result = await request(
|
|
8120
|
+
ctx,
|
|
8121
|
+
"PATCH",
|
|
8122
|
+
`/topics/${encodeURIComponent(id)}`,
|
|
8123
|
+
{ resolved }
|
|
8124
|
+
);
|
|
8125
|
+
emit(ctx, result, () => ctx.out.log(`${resolved ? "resolved" : "reopened"} ${result.id}`));
|
|
8126
|
+
}
|
|
8127
|
+
async function discussWho(ctx, parsed) {
|
|
8128
|
+
const query = new URLSearchParams({ q: stringOpt(parsed.options.q) ?? "" });
|
|
8129
|
+
const app = stringOpt(parsed.options.app);
|
|
8130
|
+
if (app) query.set("app", app);
|
|
8131
|
+
const kinds = stringOpt(parsed.options.kinds);
|
|
8132
|
+
if (kinds) query.set("kinds", kinds);
|
|
8133
|
+
const found = await request(ctx, "GET", `/mentionables?${query.toString()}`);
|
|
8134
|
+
emit(ctx, found, () => {
|
|
8135
|
+
ctx.out.log("mention kind hint");
|
|
8136
|
+
for (const item of found.items) {
|
|
8137
|
+
ctx.out.log(`@[${item.label}](${item.kind}/${item.id}) ${item.kind} ${item.hint ?? ""}`);
|
|
8138
|
+
}
|
|
8139
|
+
if (found.failed.length > 0) ctx.out.error(`(no results from: ${found.failed.join(", ")})`);
|
|
8140
|
+
});
|
|
8141
|
+
}
|
|
8142
|
+
|
|
8143
|
+
// src/discuss-watch.ts
|
|
8144
|
+
var DEFAULT_INTERVAL_MS = 15e3;
|
|
8145
|
+
var DEFAULT_TIMEOUT_MS = 15 * 6e4;
|
|
8146
|
+
function numberOpt2(parsed, flag, fallback) {
|
|
8147
|
+
const raw = stringOpt(parsed.options[flag]);
|
|
8148
|
+
const value = raw == null ? NaN : Number(raw);
|
|
8149
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
8150
|
+
}
|
|
8151
|
+
async function get(ctx, path) {
|
|
8152
|
+
const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
|
|
8153
|
+
headers: { authorization: `Bearer ${ctx.token}` }
|
|
8154
|
+
});
|
|
8155
|
+
const data = await res.json().catch(() => ({}));
|
|
8156
|
+
if (!res.ok) throw new Error(`discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`);
|
|
8157
|
+
return data;
|
|
8158
|
+
}
|
|
8159
|
+
async function discussWatch(ctx, topicId, parsed) {
|
|
8160
|
+
const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
|
|
8161
|
+
const now = ctx.now ?? Date.now;
|
|
8162
|
+
const intervalMs = numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) * 1e3;
|
|
8163
|
+
const timeoutMs = numberOpt2(parsed, "timeout", DEFAULT_TIMEOUT_MS / 1e3) * 1e3;
|
|
8164
|
+
const by = stringOpt(parsed.options.by);
|
|
8165
|
+
const self = stringOpt(parsed.options.self);
|
|
8166
|
+
const deadline = now() + timeoutMs;
|
|
8167
|
+
const fresh = (post) => (!by || post.authorId === by) && (!self || post.authorId !== self);
|
|
8168
|
+
let seen = /* @__PURE__ */ new Set();
|
|
8169
|
+
let since = now();
|
|
8170
|
+
if (topicId) {
|
|
8171
|
+
const first = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
|
|
8172
|
+
seen = new Set(first.posts.map((post) => post.id));
|
|
8173
|
+
}
|
|
8174
|
+
for (; ; ) {
|
|
8175
|
+
if (topicId) {
|
|
8176
|
+
const { posts } = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
|
|
8177
|
+
const added = posts.filter((post) => !seen.has(post.id) && fresh(post));
|
|
8178
|
+
if (added.length > 0) return report2(ctx, { found: true, posts: added });
|
|
8179
|
+
for (const post of posts) seen.add(post.id);
|
|
8180
|
+
} else {
|
|
8181
|
+
const app = stringOpt(parsed.options.app);
|
|
8182
|
+
const { topics } = await get(
|
|
8183
|
+
ctx,
|
|
8184
|
+
`/topics?state=all${app ? `&app=${encodeURIComponent(app)}` : ""}`
|
|
8185
|
+
);
|
|
8186
|
+
const active = topics.filter((topic) => topic.lastActivityAt > since);
|
|
8187
|
+
if (active.length > 0) return report2(ctx, { found: true, topics: active });
|
|
8188
|
+
since = now();
|
|
8189
|
+
}
|
|
8190
|
+
if (now() >= deadline) return report2(ctx, { found: false });
|
|
8191
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
|
|
8192
|
+
}
|
|
8193
|
+
}
|
|
8194
|
+
function report2(ctx, result) {
|
|
8195
|
+
if (ctx.json) {
|
|
8196
|
+
ctx.out.log(JSON.stringify(result, null, 2));
|
|
8197
|
+
} else if (result.found) {
|
|
8198
|
+
for (const post of result.posts ?? []) {
|
|
8199
|
+
const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
|
|
8200
|
+
ctx.out.log(`\u2014 ${who}
|
|
8201
|
+
${post.body}`);
|
|
8202
|
+
}
|
|
8203
|
+
for (const topic of result.topics ?? []) {
|
|
8204
|
+
ctx.out.log(`${topic.id} ${topic.appId ?? ""} ${topic.subject}`);
|
|
8205
|
+
}
|
|
8206
|
+
}
|
|
8207
|
+
return result;
|
|
8208
|
+
}
|
|
8209
|
+
var WatchTimeoutError = class extends Error {
|
|
8210
|
+
code = "watch_timeout";
|
|
8211
|
+
constructor() {
|
|
8212
|
+
super("nothing new before the timeout");
|
|
8213
|
+
this.name = "WatchTimeoutError";
|
|
8214
|
+
}
|
|
8215
|
+
};
|
|
8216
|
+
|
|
8217
|
+
// src/discuss-command.ts
|
|
8218
|
+
var ALLOWED = [
|
|
8219
|
+
"config",
|
|
8220
|
+
"token",
|
|
8221
|
+
"email",
|
|
8222
|
+
"json",
|
|
8223
|
+
"app",
|
|
8224
|
+
"subject",
|
|
8225
|
+
"body",
|
|
8226
|
+
"markup",
|
|
8227
|
+
"state",
|
|
8228
|
+
"q",
|
|
8229
|
+
"kinds",
|
|
8230
|
+
"limit",
|
|
8231
|
+
"offset",
|
|
8232
|
+
"reopen",
|
|
8233
|
+
"by",
|
|
8234
|
+
"self",
|
|
8235
|
+
"interval",
|
|
8236
|
+
"timeout"
|
|
8237
|
+
];
|
|
8238
|
+
function requireId(id, action) {
|
|
8239
|
+
if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
|
|
8240
|
+
return id;
|
|
8241
|
+
}
|
|
8242
|
+
async function buildContext(parsed, deps) {
|
|
8243
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
8244
|
+
const doFetch = deps.fetch ?? fetch;
|
|
8245
|
+
const out = deps.stdout ?? console;
|
|
8246
|
+
const token = await getDeveloperToken(
|
|
8247
|
+
cfg,
|
|
8248
|
+
{
|
|
8249
|
+
configPath: cfg.configPath,
|
|
8250
|
+
token: stringOpt(parsed.options.token),
|
|
8251
|
+
email: stringOpt(parsed.options.email),
|
|
8252
|
+
open: false
|
|
8253
|
+
},
|
|
8254
|
+
doFetch,
|
|
8255
|
+
out
|
|
8256
|
+
);
|
|
8257
|
+
return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
|
|
8258
|
+
}
|
|
8259
|
+
async function discussCommand(parsed, deps = {}) {
|
|
8260
|
+
assertArgs(parsed, ALLOWED, 3);
|
|
8261
|
+
const action = parsed.positionals[1];
|
|
8262
|
+
const id = parsed.positionals[2];
|
|
8263
|
+
if (!action) throw new Error('"discuss" needs an action. Run "odla-ai help".');
|
|
8264
|
+
const ctx = await buildContext(parsed, deps);
|
|
8265
|
+
switch (action) {
|
|
8266
|
+
case "groups":
|
|
8267
|
+
return discussGroups(ctx);
|
|
8268
|
+
case "list":
|
|
8269
|
+
case "topics":
|
|
8270
|
+
return discussList(ctx, parsed);
|
|
8271
|
+
case "read":
|
|
8272
|
+
return discussRead(ctx, requireId(id, "read"));
|
|
8273
|
+
case "post":
|
|
8274
|
+
return discussPost(ctx, parsed);
|
|
8275
|
+
case "reply":
|
|
8276
|
+
return discussReply(ctx, requireId(id, "reply"), parsed);
|
|
8277
|
+
case "resolve":
|
|
8278
|
+
return discussResolve(ctx, requireId(id, "resolve"), parsed.options.reopen !== true);
|
|
8279
|
+
case "who":
|
|
8280
|
+
return discussWho(ctx, parsed);
|
|
8281
|
+
case "watch": {
|
|
8282
|
+
const result = await discussWatch(ctx, id, parsed);
|
|
8283
|
+
if (!result.found) throw new WatchTimeoutError();
|
|
8284
|
+
return;
|
|
8285
|
+
}
|
|
8286
|
+
default:
|
|
8287
|
+
throw new Error(`unknown discuss action "${action}". Run "odla-ai help".`);
|
|
8288
|
+
}
|
|
8289
|
+
}
|
|
8290
|
+
|
|
7981
8291
|
// src/pm-actions.ts
|
|
7982
8292
|
var FIELD_MAP = {
|
|
7983
8293
|
title: { key: "title" },
|
|
@@ -8041,7 +8351,7 @@ function statusCol(entity, r) {
|
|
|
8041
8351
|
function printRecord(ctx, entity, r) {
|
|
8042
8352
|
ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
|
|
8043
8353
|
}
|
|
8044
|
-
function
|
|
8354
|
+
function emit2(ctx, value, human) {
|
|
8045
8355
|
if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
|
|
8046
8356
|
else human();
|
|
8047
8357
|
}
|
|
@@ -8067,7 +8377,7 @@ async function pmList(ctx, entity, parsed) {
|
|
|
8067
8377
|
}
|
|
8068
8378
|
const qs = q.toString();
|
|
8069
8379
|
const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
|
|
8070
|
-
|
|
8380
|
+
emit2(ctx, page, () => {
|
|
8071
8381
|
ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
|
|
8072
8382
|
ctx.out.log("id state app title");
|
|
8073
8383
|
for (const r of page.records) printRecord(ctx, entity, r);
|
|
@@ -8081,25 +8391,25 @@ async function pmAdd(ctx, entity, parsed) {
|
|
|
8081
8391
|
if (entity === "bug" && !input.description)
|
|
8082
8392
|
throw new Error("pm bug add needs --description <context> (or --body <context>)");
|
|
8083
8393
|
const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
|
|
8084
|
-
|
|
8394
|
+
emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
|
|
8085
8395
|
}
|
|
8086
8396
|
async function pmGet(ctx, entity, id) {
|
|
8087
8397
|
const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
|
|
8088
|
-
|
|
8398
|
+
emit2(ctx, record5, () => printRecord(ctx, entity, record5));
|
|
8089
8399
|
}
|
|
8090
8400
|
async function pmSet(ctx, entity, id, parsed) {
|
|
8091
8401
|
const patch2 = collectEntityFields(entity, parsed, true);
|
|
8092
8402
|
if (Object.keys(patch2).length === 0)
|
|
8093
8403
|
throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
|
|
8094
8404
|
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
8095
|
-
|
|
8405
|
+
emit2(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
|
|
8096
8406
|
}
|
|
8097
8407
|
async function pmDone(ctx, entity, id, parsed) {
|
|
8098
8408
|
const decisionId = stringOpt(parsed.options.decision);
|
|
8099
8409
|
if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
|
|
8100
8410
|
const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
|
|
8101
8411
|
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
8102
|
-
|
|
8412
|
+
emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
|
|
8103
8413
|
}
|
|
8104
8414
|
async function allRecords(ctx, entity, appId) {
|
|
8105
8415
|
const records = [];
|
|
@@ -8132,7 +8442,7 @@ async function pmHandoff(ctx, parsed) {
|
|
|
8132
8442
|
...handoff,
|
|
8133
8443
|
clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
|
|
8134
8444
|
};
|
|
8135
|
-
|
|
8445
|
+
emit2(ctx, result, () => {
|
|
8136
8446
|
if (result.clean) {
|
|
8137
8447
|
ctx.out.log(`${appId}: no unresolved PM work`);
|
|
8138
8448
|
return;
|
|
@@ -8165,7 +8475,7 @@ async function pmComments(ctx, entity, id) {
|
|
|
8165
8475
|
"GET",
|
|
8166
8476
|
`/${entity}/${encodeURIComponent(id)}/comments`
|
|
8167
8477
|
);
|
|
8168
|
-
|
|
8478
|
+
emit2(ctx, messages, () => {
|
|
8169
8479
|
if (messages.length === 0) ctx.out.log("(no comments)");
|
|
8170
8480
|
else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
|
|
8171
8481
|
});
|
|
@@ -8184,7 +8494,7 @@ var ALIASES = {
|
|
|
8184
8494
|
decision: "decision",
|
|
8185
8495
|
bug: "bug"
|
|
8186
8496
|
};
|
|
8187
|
-
var
|
|
8497
|
+
var ALLOWED2 = [
|
|
8188
8498
|
"config",
|
|
8189
8499
|
"token",
|
|
8190
8500
|
"email",
|
|
@@ -8208,11 +8518,11 @@ var ALLOWED = [
|
|
|
8208
8518
|
"offset",
|
|
8209
8519
|
"q"
|
|
8210
8520
|
];
|
|
8211
|
-
function
|
|
8521
|
+
function requireId2(id, action) {
|
|
8212
8522
|
if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
|
|
8213
8523
|
return id;
|
|
8214
8524
|
}
|
|
8215
|
-
async function
|
|
8525
|
+
async function buildContext2(parsed, deps) {
|
|
8216
8526
|
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
8217
8527
|
const doFetch = deps.fetch ?? fetch;
|
|
8218
8528
|
const out = deps.stdout ?? console;
|
|
@@ -8227,14 +8537,14 @@ async function buildContext(parsed, deps) {
|
|
|
8227
8537
|
async function pmCommand(parsed, deps = {}) {
|
|
8228
8538
|
const word = parsed.positionals[1] ?? "";
|
|
8229
8539
|
if (word === "handoff") {
|
|
8230
|
-
assertArgs(parsed,
|
|
8231
|
-
return pmHandoff(await
|
|
8540
|
+
assertArgs(parsed, ALLOWED2, 2);
|
|
8541
|
+
return pmHandoff(await buildContext2(parsed, deps), parsed);
|
|
8232
8542
|
}
|
|
8233
8543
|
const entity = ALIASES[word];
|
|
8234
8544
|
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
8235
8545
|
const action = parsed.positionals[2] ?? "list";
|
|
8236
|
-
assertArgs(parsed,
|
|
8237
|
-
const ctx = await
|
|
8546
|
+
assertArgs(parsed, ALLOWED2, 4);
|
|
8547
|
+
const ctx = await buildContext2(parsed, deps);
|
|
8238
8548
|
const id = parsed.positionals[3];
|
|
8239
8549
|
switch (action) {
|
|
8240
8550
|
case "list":
|
|
@@ -8243,21 +8553,21 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
8243
8553
|
case "create":
|
|
8244
8554
|
return pmAdd(ctx, entity, parsed);
|
|
8245
8555
|
case "get":
|
|
8246
|
-
return pmGet(ctx, entity,
|
|
8556
|
+
return pmGet(ctx, entity, requireId2(id, action));
|
|
8247
8557
|
case "set":
|
|
8248
8558
|
case "update":
|
|
8249
8559
|
case "status":
|
|
8250
8560
|
case "move":
|
|
8251
|
-
return pmSet(ctx, entity,
|
|
8561
|
+
return pmSet(ctx, entity, requireId2(id, action), parsed);
|
|
8252
8562
|
case "done":
|
|
8253
|
-
return pmDone(ctx, entity,
|
|
8563
|
+
return pmDone(ctx, entity, requireId2(id, action), parsed);
|
|
8254
8564
|
case "comment":
|
|
8255
|
-
return pmComment(ctx, entity,
|
|
8565
|
+
return pmComment(ctx, entity, requireId2(id, action), parsed);
|
|
8256
8566
|
case "comments":
|
|
8257
|
-
return pmComments(ctx, entity,
|
|
8567
|
+
return pmComments(ctx, entity, requireId2(id, action));
|
|
8258
8568
|
case "rm":
|
|
8259
8569
|
case "delete":
|
|
8260
|
-
return pmRemove(ctx, entity,
|
|
8570
|
+
return pmRemove(ctx, entity, requireId2(id, action));
|
|
8261
8571
|
default:
|
|
8262
8572
|
throw new Error(`unknown pm action "${action}". Try list|add|get|set|done|comment|comments|rm.`);
|
|
8263
8573
|
}
|
|
@@ -8773,7 +9083,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
8773
9083
|
return out;
|
|
8774
9084
|
}
|
|
8775
9085
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
8776
|
-
function
|
|
9086
|
+
function report3(ctx, impacts) {
|
|
8777
9087
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
8778
9088
|
ctx.out.log(
|
|
8779
9089
|
`${impacts.length} changed surface${impacts.length === 1 ? "" : "s"}; ${covered.length} covered by a runbook. Reread each one and fix any step this change made wrong.`
|
|
@@ -8811,7 +9121,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
8811
9121
|
}
|
|
8812
9122
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
8813
9123
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
8814
|
-
|
|
9124
|
+
report3(ctx, impacts);
|
|
8815
9125
|
}
|
|
8816
9126
|
|
|
8817
9127
|
// src/runbook-lint.ts
|
|
@@ -9044,7 +9354,7 @@ async function whoamiCommand(parsed, deps = {}) {
|
|
|
9044
9354
|
}
|
|
9045
9355
|
|
|
9046
9356
|
// src/runbook-command.ts
|
|
9047
|
-
var
|
|
9357
|
+
var ALLOWED3 = [
|
|
9048
9358
|
"config",
|
|
9049
9359
|
"token",
|
|
9050
9360
|
"email",
|
|
@@ -9084,7 +9394,7 @@ async function loadOrDefaultConfig(configPath, action, appId) {
|
|
|
9084
9394
|
}
|
|
9085
9395
|
return loadProjectConfig(configPath);
|
|
9086
9396
|
}
|
|
9087
|
-
async function
|
|
9397
|
+
async function buildContext3(parsed, deps, action) {
|
|
9088
9398
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
9089
9399
|
const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
|
|
9090
9400
|
const doFetch = deps.fetch ?? fetch;
|
|
@@ -9140,8 +9450,8 @@ async function buildContext2(parsed, deps, action) {
|
|
|
9140
9450
|
}
|
|
9141
9451
|
async function runbookCommand(parsed, deps = {}) {
|
|
9142
9452
|
const action = parsed.positionals[1] ?? "list";
|
|
9143
|
-
assertArgs(parsed,
|
|
9144
|
-
const ctx = await
|
|
9453
|
+
assertArgs(parsed, ALLOWED3, action === "ask" || action === "search" ? 64 : 4);
|
|
9454
|
+
const ctx = await buildContext3(parsed, deps, action);
|
|
9145
9455
|
const slug = parsed.positionals[2];
|
|
9146
9456
|
switch (action) {
|
|
9147
9457
|
case "list":
|
|
@@ -9332,31 +9642,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
9332
9642
|
url.searchParams.set("job", job.jobId);
|
|
9333
9643
|
out.log(` Studio: ${url.toString()}`);
|
|
9334
9644
|
}
|
|
9335
|
-
function printHostedReport(out,
|
|
9336
|
-
out.log(`security report ${
|
|
9337
|
-
out.log(` coverage: ${
|
|
9338
|
-
out.log(` findings: confirmed=${
|
|
9339
|
-
out.log(` discovery: ${
|
|
9340
|
-
out.log(` validation: ${
|
|
9341
|
-
for (const finding of
|
|
9645
|
+
function printHostedReport(out, report4) {
|
|
9646
|
+
out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
|
|
9647
|
+
out.log(` coverage: ${report4.coverageStatus} cells=${report4.metrics.coverageCells} shallow=${report4.metrics.shallowCells} blocked=${report4.metrics.blockedCells} unscheduled=${report4.metrics.unscheduledCells} budget_exhausted=${report4.metrics.budgetExhaustedCells}`);
|
|
9648
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
|
|
9649
|
+
out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
|
|
9650
|
+
out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
|
|
9651
|
+
for (const finding of report4.findings) {
|
|
9342
9652
|
const location = finding.locations[0];
|
|
9343
9653
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
9344
9654
|
}
|
|
9345
|
-
for (const limitation of
|
|
9655
|
+
for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
|
|
9346
9656
|
}
|
|
9347
|
-
function enforceHostedReportGate(
|
|
9657
|
+
function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
|
|
9348
9658
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
9349
9659
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
9350
9660
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
9351
9661
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
9352
|
-
const confirmed =
|
|
9353
|
-
const leads = failOnCandidates ?
|
|
9354
|
-
const incomplete =
|
|
9662
|
+
const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
9663
|
+
const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
9664
|
+
const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
9355
9665
|
if (confirmed.length || leads.length || incomplete) {
|
|
9356
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
9666
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
|
|
9357
9667
|
}
|
|
9358
9668
|
if (emitSuccess) {
|
|
9359
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
9669
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
|
|
9360
9670
|
}
|
|
9361
9671
|
}
|
|
9362
9672
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -9460,13 +9770,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
9460
9770
|
}
|
|
9461
9771
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
9462
9772
|
}
|
|
9463
|
-
const
|
|
9773
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
9464
9774
|
if (parsed.options.json === true) {
|
|
9465
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
9775
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
|
|
9466
9776
|
} else {
|
|
9467
|
-
printHostedReport(context.stdout,
|
|
9777
|
+
printHostedReport(context.stdout, report4);
|
|
9468
9778
|
}
|
|
9469
|
-
enforceHostedReportGate(
|
|
9779
|
+
enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
|
|
9470
9780
|
}
|
|
9471
9781
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
9472
9782
|
if (parsed.options.source === true) {
|
|
@@ -9507,11 +9817,11 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9507
9817
|
sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
|
|
9508
9818
|
fetch: doFetch,
|
|
9509
9819
|
stdout: out,
|
|
9510
|
-
getToken: async (
|
|
9511
|
-
if (
|
|
9820
|
+
getToken: async (request2) => {
|
|
9821
|
+
if (request2.scope === "platform:security:self") {
|
|
9512
9822
|
return getScopedPlatformToken({
|
|
9513
|
-
platform:
|
|
9514
|
-
scope:
|
|
9823
|
+
platform: request2.platform,
|
|
9824
|
+
scope: request2.scope,
|
|
9515
9825
|
email: stringOpt(parsed.options.email),
|
|
9516
9826
|
open,
|
|
9517
9827
|
fetch: doFetch,
|
|
@@ -9520,7 +9830,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9520
9830
|
});
|
|
9521
9831
|
}
|
|
9522
9832
|
const cfg = await loadProjectConfig(configPath);
|
|
9523
|
-
if (platformAudience(cfg.platformUrl) !== platformAudience(
|
|
9833
|
+
if (platformAudience(cfg.platformUrl) !== platformAudience(request2.platform)) {
|
|
9524
9834
|
throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
|
|
9525
9835
|
}
|
|
9526
9836
|
return getDeveloperToken(
|
|
@@ -9533,13 +9843,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9533
9843
|
});
|
|
9534
9844
|
enforceLocalGate(result.report, parsed);
|
|
9535
9845
|
}
|
|
9536
|
-
function enforceLocalGate(
|
|
9846
|
+
function enforceLocalGate(report4, parsed) {
|
|
9537
9847
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
9538
9848
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
9539
9849
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
9540
|
-
const confirmed = findingsAtOrAbove(
|
|
9541
|
-
const leads = failOnCandidates ? findingsAtOrAbove(
|
|
9542
|
-
const incomplete =
|
|
9850
|
+
const confirmed = findingsAtOrAbove(report4, failOn);
|
|
9851
|
+
const leads = failOnCandidates ? findingsAtOrAbove(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
9852
|
+
const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
9543
9853
|
if (confirmed.length || leads.length || incomplete) {
|
|
9544
9854
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
9545
9855
|
}
|
|
@@ -9578,9 +9888,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
9578
9888
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
9579
9889
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
9580
9890
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
9581
|
-
const
|
|
9582
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
9583
|
-
else printHostedReport(context.stdout,
|
|
9891
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId });
|
|
9892
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
|
|
9893
|
+
else printHostedReport(context.stdout, report4);
|
|
9584
9894
|
return;
|
|
9585
9895
|
}
|
|
9586
9896
|
if (sub !== "run") {
|
|
@@ -9664,7 +9974,8 @@ async function securityStatus(parsed, dependencies) {
|
|
|
9664
9974
|
|
|
9665
9975
|
// src/cli.ts
|
|
9666
9976
|
function exitCodeFor(err) {
|
|
9667
|
-
|
|
9977
|
+
const code = err?.code;
|
|
9978
|
+
return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
|
|
9668
9979
|
}
|
|
9669
9980
|
async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
9670
9981
|
const parsed = parseArgv(argv);
|
|
@@ -9712,6 +10023,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
9712
10023
|
await pmCommand(parsed, dependencies);
|
|
9713
10024
|
return;
|
|
9714
10025
|
}
|
|
10026
|
+
if (command === "discuss") {
|
|
10027
|
+
await discussCommand(parsed, dependencies);
|
|
10028
|
+
return;
|
|
10029
|
+
}
|
|
9715
10030
|
if (command === "provision") {
|
|
9716
10031
|
await provisionCommand(parsed, dependencies);
|
|
9717
10032
|
return;
|
|
@@ -9831,4 +10146,4 @@ export {
|
|
|
9831
10146
|
exitCodeFor,
|
|
9832
10147
|
runCli
|
|
9833
10148
|
};
|
|
9834
|
-
//# sourceMappingURL=chunk-
|
|
10149
|
+
//# sourceMappingURL=chunk-HG63GLGU.js.map
|