@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
package/dist/bin.cjs
CHANGED
|
@@ -553,23 +553,23 @@ function adminAiAuditQuery(filters) {
|
|
|
553
553
|
}
|
|
554
554
|
return `?limit=${filters.limit}`;
|
|
555
555
|
}
|
|
556
|
-
async function readAdminAiAudit(
|
|
557
|
-
const response2 = await
|
|
558
|
-
headers:
|
|
556
|
+
async function readAdminAiAudit(request2) {
|
|
557
|
+
const response2 = await request2.fetch(`${request2.platform}/registry/platform/ai-audit${request2.query}`, {
|
|
558
|
+
headers: request2.headers
|
|
559
559
|
});
|
|
560
560
|
const body = await responseBody(response2);
|
|
561
561
|
if (!response2.ok) throw new Error(apiError(response2.status, body));
|
|
562
|
-
if (
|
|
563
|
-
|
|
562
|
+
if (request2.json) {
|
|
563
|
+
request2.stdout.log(JSON.stringify(body, null, 2));
|
|
564
564
|
return;
|
|
565
565
|
}
|
|
566
566
|
const events = isRecord(body) && Array.isArray(body.events) ? body.events.filter(isRecord) : [];
|
|
567
|
-
|
|
567
|
+
request2.stdout.log("when change target before -> after actor");
|
|
568
568
|
for (const event of events) {
|
|
569
569
|
const before = isRecord(event.oldPolicy) ? event.oldPolicy : void 0;
|
|
570
570
|
const after = isRecord(event.newPolicy) ? event.newPolicy : void 0;
|
|
571
571
|
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";
|
|
572
|
-
|
|
572
|
+
request2.stdout.log([
|
|
573
573
|
timestamp(event.createdAt),
|
|
574
574
|
String(event.changeKind ?? ""),
|
|
575
575
|
String(event.purpose ?? event.provider ?? ""),
|
|
@@ -625,14 +625,14 @@ function adminAiUsageQuery(filters) {
|
|
|
625
625
|
const query = params.toString();
|
|
626
626
|
return query ? `?${query}` : "";
|
|
627
627
|
}
|
|
628
|
-
async function readAdminAiUsage(
|
|
629
|
-
const res = await
|
|
630
|
-
headers:
|
|
628
|
+
async function readAdminAiUsage(request2) {
|
|
629
|
+
const res = await request2.fetch(`${request2.platform}/registry/platform/ai-usage${request2.query}`, {
|
|
630
|
+
headers: request2.headers
|
|
631
631
|
});
|
|
632
632
|
const body = await responseBody2(res);
|
|
633
633
|
if (!res.ok) throw new Error(apiError2("read platform AI usage", res.status, body));
|
|
634
|
-
if (
|
|
635
|
-
else printUsage(body,
|
|
634
|
+
if (request2.json) request2.stdout.log(JSON.stringify(body, null, 2));
|
|
635
|
+
else printUsage(body, request2.stdout);
|
|
636
636
|
}
|
|
637
637
|
function usageLimit(value) {
|
|
638
638
|
if (!Number.isSafeInteger(value) || value < 1 || value > 500) {
|
|
@@ -3094,10 +3094,10 @@ function installSkill(options = {}) {
|
|
|
3094
3094
|
current.add(target);
|
|
3095
3095
|
targets.set(harness, current);
|
|
3096
3096
|
};
|
|
3097
|
-
const plan = (target,
|
|
3097
|
+
const plan = (target, content2, managedMerge = false, boundary = root) => {
|
|
3098
3098
|
const existing = plans.get(target);
|
|
3099
|
-
if (existing && existing.content !==
|
|
3100
|
-
plans.set(target, { target, content, boundary, managedMerge });
|
|
3099
|
+
if (existing && existing.content !== content2) throw new Error(`internal agent setup conflict for ${target}`);
|
|
3100
|
+
plans.set(target, { target, content: content2, boundary, managedMerge });
|
|
3101
3101
|
};
|
|
3102
3102
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
3103
3103
|
for (const rel of files) plan((0, import_node_path8.join)(targetDir2, rel), (0, import_node_fs10.readFileSync)((0, import_node_path8.join)(sourceDir, rel), "utf8"), false, boundary);
|
|
@@ -3883,11 +3883,11 @@ async function gitBlobs(cwd, entries, maxBytes) {
|
|
|
3883
3883
|
if (!Number.isSafeInteger(length) || length < 0 || newline + 1 + length >= output.length) {
|
|
3884
3884
|
throw new Error("git object output has an invalid length");
|
|
3885
3885
|
}
|
|
3886
|
-
const
|
|
3886
|
+
const content2 = output.subarray(newline + 1, newline + 1 + length);
|
|
3887
3887
|
if (output[newline + 1 + length] !== 10) throw new Error("git object output is malformed");
|
|
3888
|
-
total +=
|
|
3888
|
+
total += content2.byteLength;
|
|
3889
3889
|
if (total > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
3890
|
-
blobs.push(
|
|
3890
|
+
blobs.push(content2);
|
|
3891
3891
|
offset = newline + 1 + length + 1;
|
|
3892
3892
|
}
|
|
3893
3893
|
return blobs;
|
|
@@ -3910,13 +3910,13 @@ async function materializeGitTree(source, commitSha, options = {}) {
|
|
|
3910
3910
|
try {
|
|
3911
3911
|
const blobs = await gitBlobs(sourceDir, entries, maxBytes);
|
|
3912
3912
|
for (const [index, entry] of entries.entries()) {
|
|
3913
|
-
const
|
|
3914
|
-
byteCount +=
|
|
3913
|
+
const content2 = blobs[index];
|
|
3914
|
+
byteCount += content2.byteLength;
|
|
3915
3915
|
if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
3916
3916
|
const target = (0, import_path2.resolve)(targetRoot, entry.path);
|
|
3917
3917
|
if (!target.startsWith(`${(0, import_path2.resolve)(targetRoot)}${import_path2.sep}`)) throw new TypeError("Git tree path escapes workspace");
|
|
3918
3918
|
await (0, import_promises3.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
|
|
3919
|
-
await (0, import_promises3.writeFile)(target,
|
|
3919
|
+
await (0, import_promises3.writeFile)(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
|
|
3920
3920
|
}
|
|
3921
3921
|
return {
|
|
3922
3922
|
root,
|
|
@@ -4294,11 +4294,11 @@ var ID2 = /^[A-Za-z0-9._:-]{1,180}$/;
|
|
|
4294
4294
|
var MAX_PATCH_BYTES = 256 * 1024;
|
|
4295
4295
|
var MAX_STATE_BYTES = 64 * 1024;
|
|
4296
4296
|
async function createCodePortableCheckpoint(input) {
|
|
4297
|
-
const
|
|
4297
|
+
const state2 = normalizeState(input.state);
|
|
4298
4298
|
validateBaseAndPatch(input.baseCommitSha, input.patch);
|
|
4299
4299
|
const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
|
|
4300
|
-
const stateDigest = `sha256:${await sha256Hex(canonicalJson(
|
|
4301
|
-
const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state, stateDigest };
|
|
4300
|
+
const stateDigest = `sha256:${await sha256Hex(canonicalJson(state2))}`;
|
|
4301
|
+
const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state: state2, stateDigest };
|
|
4302
4302
|
const checkpointDigest = `sha256:${await sha256Hex(canonicalJson(fields))}`;
|
|
4303
4303
|
return freeze({ ...fields, patch: input.patch, checkpointDigest });
|
|
4304
4304
|
}
|
|
@@ -4319,8 +4319,8 @@ async function verifyCodePortableCheckpoint(value) {
|
|
|
4319
4319
|
return created;
|
|
4320
4320
|
}
|
|
4321
4321
|
function normalizeState(value) {
|
|
4322
|
-
const
|
|
4323
|
-
exact2(
|
|
4322
|
+
const state2 = object(value, "state");
|
|
4323
|
+
exact2(state2, [
|
|
4324
4324
|
"planCursor",
|
|
4325
4325
|
"conversationRefs",
|
|
4326
4326
|
"planningInputDigest",
|
|
@@ -4332,13 +4332,13 @@ function normalizeState(value) {
|
|
|
4332
4332
|
"unresolvedApprovals",
|
|
4333
4333
|
"trustStatus"
|
|
4334
4334
|
]);
|
|
4335
|
-
const planCursor =
|
|
4336
|
-
const conversations = strings(
|
|
4337
|
-
const approvals = strings(
|
|
4338
|
-
if (planCursor !== null && (typeof planCursor !== "string" || !ID2.test(planCursor)) || typeof
|
|
4335
|
+
const planCursor = state2.planCursor;
|
|
4336
|
+
const conversations = strings(state2.conversationRefs, "conversationRefs", ID2, 256, false);
|
|
4337
|
+
const approvals = strings(state2.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
|
|
4338
|
+
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) {
|
|
4339
4339
|
throw invalid2("Portable checkpoint state is malformed or outside its bounds.");
|
|
4340
4340
|
}
|
|
4341
|
-
const effects =
|
|
4341
|
+
const effects = state2.completedEffects.map((item) => {
|
|
4342
4342
|
const effect = object(item, "completed effect");
|
|
4343
4343
|
exact2(effect, ["effectId", "actionDigest", "receiptDigest"]);
|
|
4344
4344
|
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)) {
|
|
@@ -4353,18 +4353,18 @@ function normalizeState(value) {
|
|
|
4353
4353
|
if (new Set(effects.map((item) => item.effectId)).size !== effects.length) {
|
|
4354
4354
|
throw invalid2("Portable checkpoint repeats a completed effect.");
|
|
4355
4355
|
}
|
|
4356
|
-
const trustStatus =
|
|
4357
|
-
if (trustStatus === "candidate_untrusted" && (
|
|
4356
|
+
const trustStatus = state2.trustStatus;
|
|
4357
|
+
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)) {
|
|
4358
4358
|
throw invalid2("Portable checkpoint trust status does not match its evidence digests.");
|
|
4359
4359
|
}
|
|
4360
4360
|
const normalized = {
|
|
4361
4361
|
planCursor,
|
|
4362
4362
|
conversationRefs: conversations,
|
|
4363
|
-
planningInputDigest:
|
|
4364
|
-
buildPolicyDigest:
|
|
4365
|
-
dependencyLayerDigest:
|
|
4366
|
-
verificationDigest:
|
|
4367
|
-
reviewDigest:
|
|
4363
|
+
planningInputDigest: state2.planningInputDigest,
|
|
4364
|
+
buildPolicyDigest: state2.buildPolicyDigest,
|
|
4365
|
+
dependencyLayerDigest: state2.dependencyLayerDigest,
|
|
4366
|
+
verificationDigest: state2.verificationDigest,
|
|
4367
|
+
reviewDigest: state2.reviewDigest,
|
|
4368
4368
|
completedEffects: effects,
|
|
4369
4369
|
unresolvedApprovals: approvals,
|
|
4370
4370
|
trustStatus
|
|
@@ -4481,60 +4481,60 @@ async function createConversionRegistry(config) {
|
|
|
4481
4481
|
policies.set(policy.conversionId, Object.freeze(policy));
|
|
4482
4482
|
}
|
|
4483
4483
|
const outputCounts = /* @__PURE__ */ new Map();
|
|
4484
|
-
const
|
|
4484
|
+
const get2 = (id, kind) => {
|
|
4485
4485
|
const policy = policies.get(id);
|
|
4486
4486
|
if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
|
|
4487
4487
|
return policy;
|
|
4488
4488
|
};
|
|
4489
4489
|
const checked = (source, id, kind) => {
|
|
4490
|
-
const policy =
|
|
4490
|
+
const policy = get2(id, kind);
|
|
4491
4491
|
if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
|
|
4492
4492
|
return policy;
|
|
4493
4493
|
};
|
|
4494
|
-
const
|
|
4494
|
+
const emit3 = (source, policy, value) => convert(source, policy, value, outputCounts);
|
|
4495
4495
|
const operations = Object.freeze({
|
|
4496
4496
|
boolean: async (value, id) => {
|
|
4497
4497
|
const policy = checked(value, id, "boolean");
|
|
4498
|
-
return
|
|
4498
|
+
return emit3(value, policy, requireBoolean(value.value));
|
|
4499
4499
|
},
|
|
4500
4500
|
integer: async (value, id) => {
|
|
4501
4501
|
const policy = checked(value, id, "integer");
|
|
4502
|
-
return
|
|
4502
|
+
return emit3(value, policy, boundedInteger(value.value, policy.output));
|
|
4503
4503
|
},
|
|
4504
4504
|
finiteNumber: async (value, id) => {
|
|
4505
4505
|
const policy = checked(value, id, "finite_number");
|
|
4506
|
-
return
|
|
4506
|
+
return emit3(value, policy, boundedNumber(value.value, policy.output));
|
|
4507
4507
|
},
|
|
4508
4508
|
enum: async (value, id) => {
|
|
4509
4509
|
const policy = checked(value, id, "enum");
|
|
4510
|
-
return
|
|
4510
|
+
return emit3(value, policy, enumMember(value.value, policy.output));
|
|
4511
4511
|
},
|
|
4512
4512
|
date: async (value, id) => {
|
|
4513
4513
|
const policy = checked(value, id, "date");
|
|
4514
|
-
return
|
|
4514
|
+
return emit3(value, policy, canonicalDate(value.value, policy.output));
|
|
4515
4515
|
},
|
|
4516
4516
|
registeredId: async (value, id) => {
|
|
4517
4517
|
const policy = checked(value, id, "registered_id");
|
|
4518
4518
|
const registry = config.registeredIds?.[policy.output.registryId];
|
|
4519
4519
|
const output = typeof value.value === "string" ? registry?.values[value.value] : void 0;
|
|
4520
4520
|
if (!output) throw new CamelError("conversion_rejected", "Registered-ID conversion rejected the candidate.");
|
|
4521
|
-
return
|
|
4521
|
+
return emit3(value, policy, output);
|
|
4522
4522
|
},
|
|
4523
4523
|
digest: async (value, id) => {
|
|
4524
4524
|
const policy = checked(value, id, "digest");
|
|
4525
4525
|
if (!(value.value instanceof Uint8Array)) throw new CamelError("conversion_rejected", "Digest conversion requires bytes.");
|
|
4526
|
-
return
|
|
4526
|
+
return emit3(value, policy, await sha256Hex(value.value));
|
|
4527
4527
|
},
|
|
4528
4528
|
measure: async (value, metric, id) => {
|
|
4529
4529
|
const policy = checked(value, id, "integer");
|
|
4530
4530
|
const measured = measure(value.value, metric);
|
|
4531
|
-
return
|
|
4531
|
+
return emit3(value, policy, boundedInteger(measured, policy.output));
|
|
4532
4532
|
},
|
|
4533
4533
|
test: async (value, predicateId, id) => {
|
|
4534
4534
|
const policy = checked(value, id, "boolean");
|
|
4535
4535
|
const predicate = config.predicates?.[predicateId];
|
|
4536
4536
|
if (!predicate) throw new CamelError("conversion_rejected", "Predicate is not registered.");
|
|
4537
|
-
return
|
|
4537
|
+
return emit3(value, policy, evaluatePredicate(value.value, predicate, config.registeredIds));
|
|
4538
4538
|
}
|
|
4539
4539
|
});
|
|
4540
4540
|
return Object.freeze({ operations, policy: (id) => policies.get(id) ?? missingPolicy() });
|
|
@@ -4753,11 +4753,11 @@ async function digestStagedWorkspace(root, limits) {
|
|
|
4753
4753
|
const hash = (0, import_crypto.createHash)("sha256");
|
|
4754
4754
|
let bytes = 0;
|
|
4755
4755
|
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
4756
|
-
const
|
|
4757
|
-
bytes += Buffer.byteLength(file.path) +
|
|
4756
|
+
const content2 = await (0, import_promises5.readFile)(file.target);
|
|
4757
|
+
bytes += Buffer.byteLength(file.path) + content2.byteLength;
|
|
4758
4758
|
if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
|
|
4759
|
-
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${
|
|
4760
|
-
hash.update(
|
|
4759
|
+
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content2.byteLength}:`);
|
|
4760
|
+
hash.update(content2);
|
|
4761
4761
|
}
|
|
4762
4762
|
return `sha256:${hash.digest("hex")}`;
|
|
4763
4763
|
}
|
|
@@ -4852,13 +4852,13 @@ function createCodeRuntimeControlClient(options) {
|
|
|
4852
4852
|
if (!Number.isSafeInteger(modelRequestTimeoutMs) || modelRequestTimeoutMs < 3e4 || modelRequestTimeoutMs > 30 * 6e4) {
|
|
4853
4853
|
throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
|
|
4854
4854
|
}
|
|
4855
|
-
const
|
|
4855
|
+
const request2 = options.fetch ?? fetch;
|
|
4856
4856
|
const call2 = async (path, body, timeoutMs = requestTimeoutMs) => {
|
|
4857
4857
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
4858
4858
|
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
4859
4859
|
let response2;
|
|
4860
4860
|
try {
|
|
4861
|
-
response2 = await
|
|
4861
|
+
response2 = await request2(`${endpoint}${path}`, {
|
|
4862
4862
|
method: "POST",
|
|
4863
4863
|
headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json" },
|
|
4864
4864
|
body: JSON.stringify(body),
|
|
@@ -5765,34 +5765,34 @@ function createCodeToolBroker(options) {
|
|
|
5765
5765
|
const policy = createCodePolicyGate(options);
|
|
5766
5766
|
let tail = Promise.resolve();
|
|
5767
5767
|
return {
|
|
5768
|
-
execute(context,
|
|
5769
|
-
const result = tail.then(() => route(context,
|
|
5768
|
+
execute(context, request2) {
|
|
5769
|
+
const result = tail.then(() => route(context, request2, options, recipes, policy));
|
|
5770
5770
|
tail = result.then(() => void 0, () => void 0);
|
|
5771
5771
|
return result;
|
|
5772
5772
|
}
|
|
5773
5773
|
};
|
|
5774
5774
|
}
|
|
5775
|
-
async function route(context,
|
|
5775
|
+
async function route(context, request2, options, recipes, policy) {
|
|
5776
5776
|
try {
|
|
5777
5777
|
if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
|
|
5778
|
-
if (
|
|
5779
|
-
if (
|
|
5780
|
-
return await recipe(context,
|
|
5778
|
+
if (request2.tool === "sandbox.read") return await read(context, request2, options, policy);
|
|
5779
|
+
if (request2.tool === "sandbox.apply_patch") return await patch(context, request2, options, policy);
|
|
5780
|
+
return await recipe(context, request2, options, recipes, policy);
|
|
5781
5781
|
} catch (reason) {
|
|
5782
|
-
return response(
|
|
5782
|
+
return response(request2, false, reason instanceof TypeError ? reason.message : "tool failed closed");
|
|
5783
5783
|
}
|
|
5784
5784
|
}
|
|
5785
|
-
async function read(context,
|
|
5786
|
-
exactKeys(
|
|
5787
|
-
const path = stringField(
|
|
5788
|
-
const startLine = optionalInteger(
|
|
5789
|
-
const endLine = optionalInteger(
|
|
5785
|
+
async function read(context, request2, options, policy) {
|
|
5786
|
+
exactKeys(request2.input, ["path", "startLine", "endLine"]);
|
|
5787
|
+
const path = stringField(request2.input, "path");
|
|
5788
|
+
const startLine = optionalInteger(request2.input.startLine) ?? 1;
|
|
5789
|
+
const endLine = optionalInteger(request2.input.endLine) ?? startLine + (options.maxReadLines ?? 2e3) - 1;
|
|
5790
5790
|
if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
|
|
5791
5791
|
throw new TypeError("requested line range exceeds its bound");
|
|
5792
5792
|
}
|
|
5793
5793
|
const paths = await registeredFiles(context.workspaceDir, 2e4);
|
|
5794
|
-
const allowed = await policy.read(policyContext(context,
|
|
5795
|
-
if (!allowed) return response(
|
|
5794
|
+
const allowed = await policy.read(policyContext(context, request2, options, { paths, path, startLine, endLine }));
|
|
5795
|
+
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
5796
5796
|
const target = resolveCodePath(context.workspaceDir, path);
|
|
5797
5797
|
const info = await (0, import_promises9.stat)(target);
|
|
5798
5798
|
if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
|
|
@@ -5801,40 +5801,40 @@ async function read(context, request, options, policy) {
|
|
|
5801
5801
|
const source = await (0, import_promises9.readFile)(target);
|
|
5802
5802
|
if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
|
|
5803
5803
|
const lines = source.toString("utf8").split("\n");
|
|
5804
|
-
const
|
|
5805
|
-
if (Buffer.byteLength(
|
|
5804
|
+
const content2 = lines.slice(startLine - 1, endLine).join("\n");
|
|
5805
|
+
if (Buffer.byteLength(content2) > (options.maxReadBytes ?? 128 * 1024)) {
|
|
5806
5806
|
throw new TypeError("read result exceeds its byte bound");
|
|
5807
5807
|
}
|
|
5808
|
-
return response(
|
|
5808
|
+
return response(request2, true, content2, { path, startLine, endLine: Math.min(endLine, lines.length) });
|
|
5809
5809
|
}
|
|
5810
|
-
async function patch(context,
|
|
5811
|
-
exactKeys(
|
|
5812
|
-
const value = stringField(
|
|
5810
|
+
async function patch(context, request2, options, policy) {
|
|
5811
|
+
exactKeys(request2.input, ["patch"]);
|
|
5812
|
+
const value = stringField(request2.input, "patch");
|
|
5813
5813
|
const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
|
|
5814
5814
|
if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
|
|
5815
5815
|
throw new TypeError("patch targets a read-only reference source");
|
|
5816
5816
|
}
|
|
5817
|
-
const allowed = await policy.patch(policyContext(context,
|
|
5818
|
-
if (!allowed) return response(
|
|
5817
|
+
const allowed = await policy.patch(policyContext(context, request2, options, { patch: value }));
|
|
5818
|
+
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
5819
5819
|
await applyCodePatch(context.workspaceDir, value, paths);
|
|
5820
|
-
return response(
|
|
5820
|
+
return response(request2, true, `Applied patch to ${paths.length} file(s).`, { paths });
|
|
5821
5821
|
}
|
|
5822
|
-
async function recipe(context,
|
|
5823
|
-
exactKeys(
|
|
5824
|
-
const recipeId = stringField(
|
|
5822
|
+
async function recipe(context, request2, options, recipes, policy) {
|
|
5823
|
+
exactKeys(request2.input, ["recipeId"]);
|
|
5824
|
+
const recipeId = stringField(request2.input, "recipeId");
|
|
5825
5825
|
const digestLimits = {
|
|
5826
5826
|
maxFiles: options.maxRecipeWorkspaceFiles ?? 2e4,
|
|
5827
5827
|
maxBytes: options.maxRecipeWorkspaceBytes ?? 512 * 1024 * 1024
|
|
5828
5828
|
};
|
|
5829
5829
|
const sourceDigest = await digestStagedWorkspace(context.workspaceDir, digestLimits);
|
|
5830
|
-
const allowed = await policy.recipe(policyContext(context,
|
|
5830
|
+
const allowed = await policy.recipe(policyContext(context, request2, options, {
|
|
5831
5831
|
recipeIds: [...recipes.keys()].sort(),
|
|
5832
5832
|
recipeId,
|
|
5833
5833
|
sourceDigest
|
|
5834
5834
|
}));
|
|
5835
|
-
if (!allowed) return response(
|
|
5835
|
+
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
5836
5836
|
const selected = recipes.get(recipeId);
|
|
5837
|
-
if (!selected) return response(
|
|
5837
|
+
if (!selected) return response(request2, false, "build recipe is not registered");
|
|
5838
5838
|
const staged = await stageWorkspace(context.workspaceDir, {
|
|
5839
5839
|
maxFiles: digestLimits.maxFiles,
|
|
5840
5840
|
maxBytes: digestLimits.maxBytes
|
|
@@ -5851,7 +5851,7 @@ async function recipe(context, request, options, recipes, policy) {
|
|
|
5851
5851
|
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
5852
5852
|
const ok = result.exitCode === 0 && !result.outputLimitExceeded && !result.timedOut;
|
|
5853
5853
|
const status = result.timedOut ? "timed out" : result.outputLimitExceeded ? "exceeded output limit" : ok ? "passed" : `failed with exit ${result.exitCode}`;
|
|
5854
|
-
return response(
|
|
5854
|
+
return response(request2, ok, `Recipe ${recipeId} ${status}.${output ? `
|
|
5855
5855
|
${output}` : ""}`, {
|
|
5856
5856
|
recipeId,
|
|
5857
5857
|
exitCode: result.exitCode,
|
|
@@ -5863,10 +5863,10 @@ ${output}` : ""}`, {
|
|
|
5863
5863
|
await staged.cleanup();
|
|
5864
5864
|
}
|
|
5865
5865
|
}
|
|
5866
|
-
function policyContext(context,
|
|
5866
|
+
function policyContext(context, request2, options, extra) {
|
|
5867
5867
|
return {
|
|
5868
5868
|
lease: context.lease,
|
|
5869
|
-
request,
|
|
5869
|
+
request: request2,
|
|
5870
5870
|
workspaceId: `workspace:${context.lease.task.attemptId}`,
|
|
5871
5871
|
readers: { kind: "principals", principalIds: [options.readerId] },
|
|
5872
5872
|
...extra
|
|
@@ -5916,8 +5916,8 @@ function optionalInteger(value) {
|
|
|
5916
5916
|
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("line bounds must be positive integers");
|
|
5917
5917
|
return value;
|
|
5918
5918
|
}
|
|
5919
|
-
function response(
|
|
5920
|
-
return { requestId:
|
|
5919
|
+
function response(request2, ok, content2, details) {
|
|
5920
|
+
return { requestId: request2.requestId, ok, content: content2, ...details ? { details } : {} };
|
|
5921
5921
|
}
|
|
5922
5922
|
function codeCommandMetadata(payload, resume) {
|
|
5923
5923
|
const trusted = record22(payload.trustedBase);
|
|
@@ -6017,13 +6017,13 @@ function createCodeRuntimeToolBroker(input, lease, role) {
|
|
|
6017
6017
|
readerId: `code-session:${lease.task.taskId}`,
|
|
6018
6018
|
readOnlyPrefixes: [".odla-references"]
|
|
6019
6019
|
});
|
|
6020
|
-
return role === "coding" ? broker : { execute: (context,
|
|
6020
|
+
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" }) };
|
|
6021
6021
|
}
|
|
6022
6022
|
async function handleCodeRuntimeInference(input) {
|
|
6023
|
-
const { command, metadata: metadata2, request, state } = input;
|
|
6024
|
-
if (
|
|
6025
|
-
if (!
|
|
6026
|
-
|
|
6023
|
+
const { command, metadata: metadata2, request: request2, state: state2 } = input;
|
|
6024
|
+
if (state2.tokens >= metadata2.maxTokensPerInteraction) {
|
|
6025
|
+
if (!state2.noticeEmitted) {
|
|
6026
|
+
state2.noticeEmitted = true;
|
|
6027
6027
|
await input.event({
|
|
6028
6028
|
type: "message",
|
|
6029
6029
|
actor: "system",
|
|
@@ -6033,7 +6033,7 @@ async function handleCodeRuntimeInference(input) {
|
|
|
6033
6033
|
return {
|
|
6034
6034
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
6035
6035
|
type: "inference.response",
|
|
6036
|
-
requestId:
|
|
6036
|
+
requestId: request2.requestId,
|
|
6037
6037
|
response: {
|
|
6038
6038
|
id: `budget:${command.commandId}`,
|
|
6039
6039
|
provider: "openai",
|
|
@@ -6047,11 +6047,11 @@ async function handleCodeRuntimeInference(input) {
|
|
|
6047
6047
|
}
|
|
6048
6048
|
const startedAt = Date.now();
|
|
6049
6049
|
const response2 = await input.control.infer(command.sessionId, {
|
|
6050
|
-
requestId:
|
|
6050
|
+
requestId: request2.requestId,
|
|
6051
6051
|
interactionId: command.commandId,
|
|
6052
|
-
call:
|
|
6052
|
+
call: request2.call
|
|
6053
6053
|
});
|
|
6054
|
-
|
|
6054
|
+
state2.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
|
|
6055
6055
|
await input.event({
|
|
6056
6056
|
type: "usage",
|
|
6057
6057
|
provider: response2.receipt.provider,
|
|
@@ -6060,13 +6060,13 @@ async function handleCodeRuntimeInference(input) {
|
|
|
6060
6060
|
outputTokens: response2.receipt.outputTokens,
|
|
6061
6061
|
durationMs: Date.now() - startedAt,
|
|
6062
6062
|
interactionId: command.commandId,
|
|
6063
|
-
interactionTokens:
|
|
6063
|
+
interactionTokens: state2.tokens,
|
|
6064
6064
|
interactionMaxTokens: metadata2.maxTokensPerInteraction
|
|
6065
6065
|
}).catch(() => void 0);
|
|
6066
6066
|
return {
|
|
6067
6067
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
6068
6068
|
type: "inference.response",
|
|
6069
|
-
requestId:
|
|
6069
|
+
requestId: request2.requestId,
|
|
6070
6070
|
response: response2.response
|
|
6071
6071
|
};
|
|
6072
6072
|
}
|
|
@@ -6434,6 +6434,14 @@ Usage:
|
|
|
6434
6434
|
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
6435
6435
|
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
6436
6436
|
odla-ai pm handoff --app <id> [--json]
|
|
6437
|
+
odla-ai discuss groups [--json]
|
|
6438
|
+
odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
|
|
6439
|
+
odla-ai discuss read <topic> [--json]
|
|
6440
|
+
odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"]
|
|
6441
|
+
odla-ai discuss reply <topic> --body "..." [--markup "..."]
|
|
6442
|
+
odla-ai discuss resolve <topic> [--reopen]
|
|
6443
|
+
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
6444
|
+
odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
|
|
6437
6445
|
odla-ai whoami [--json]
|
|
6438
6446
|
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
6439
6447
|
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
@@ -6538,6 +6546,13 @@ Commands:
|
|
|
6538
6546
|
"app". Entities: goal (alias conformance), task (alias kanban),
|
|
6539
6547
|
decision, bug. Status changes and comments post to each item's
|
|
6540
6548
|
@odla-ai/chat discussion thread.
|
|
6549
|
+
discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
|
|
6550
|
+
one group per project, topics with replies, @-mentions of people,
|
|
6551
|
+
agents, PM items, and projects. Built for unattended use \u2014 post a
|
|
6552
|
+
question, then "watch" it until someone answers. "watch" exits 75
|
|
6553
|
+
(not 1) when it times out with nothing new, so a script can tell
|
|
6554
|
+
"no answer yet" from a failure and just rerun. Use "who" to look
|
|
6555
|
+
up the exact @[Label](kind/id) markup for a mention.
|
|
6541
6556
|
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
6542
6557
|
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
6543
6558
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
@@ -6725,15 +6740,15 @@ async function connectGitHubSecuritySource(options) {
|
|
|
6725
6740
|
const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
|
|
6726
6741
|
const wait2 = options.wait ?? waitForHostedPoll;
|
|
6727
6742
|
while (true) {
|
|
6728
|
-
const
|
|
6743
|
+
const state2 = await requestHostedSecurityJson(
|
|
6729
6744
|
options,
|
|
6730
6745
|
`/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
|
|
6731
6746
|
{},
|
|
6732
6747
|
"check GitHub connection"
|
|
6733
6748
|
);
|
|
6734
|
-
if (
|
|
6735
|
-
if (
|
|
6736
|
-
throw new Error(`GitHub connection ${
|
|
6749
|
+
if (state2.status !== "pending") {
|
|
6750
|
+
if (state2.status === "connected") return state2;
|
|
6751
|
+
throw new Error(`GitHub connection ${state2.status}${state2.failureCode ? `: ${state2.failureCode}` : ""}`);
|
|
6737
6752
|
}
|
|
6738
6753
|
if (now() >= deadline) throw new Error("GitHub connection approval timed out");
|
|
6739
6754
|
await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
@@ -7184,6 +7199,289 @@ async function codeCommand(parsed, dependencies) {
|
|
|
7184
7199
|
});
|
|
7185
7200
|
}
|
|
7186
7201
|
|
|
7202
|
+
// src/discuss-actions.ts
|
|
7203
|
+
async function request(ctx, method, path, body) {
|
|
7204
|
+
const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
|
|
7205
|
+
method,
|
|
7206
|
+
headers: { authorization: `Bearer ${ctx.token}`, "content-type": "application/json" },
|
|
7207
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
7208
|
+
});
|
|
7209
|
+
const data = await res.json().catch(() => ({}));
|
|
7210
|
+
if (!res.ok)
|
|
7211
|
+
throw new Error(`discuss ${method} ${path} failed: ${data.error ?? `registry returned ${res.status}`}`);
|
|
7212
|
+
return data;
|
|
7213
|
+
}
|
|
7214
|
+
function emit(ctx, value, human) {
|
|
7215
|
+
if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
|
|
7216
|
+
else human();
|
|
7217
|
+
}
|
|
7218
|
+
var state = (topic) => topic.resolvedAt ? "resolved" : "open";
|
|
7219
|
+
function bodyWithRefs(post) {
|
|
7220
|
+
if (!post.refs || post.refs.length === 0) return post.body;
|
|
7221
|
+
let out = "";
|
|
7222
|
+
let cursor = 0;
|
|
7223
|
+
for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
|
|
7224
|
+
if (ref.start < cursor || ref.end > post.body.length) continue;
|
|
7225
|
+
out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
|
|
7226
|
+
cursor = ref.end;
|
|
7227
|
+
}
|
|
7228
|
+
return out + post.body.slice(cursor);
|
|
7229
|
+
}
|
|
7230
|
+
function content(parsed) {
|
|
7231
|
+
const markup = stringOpt(parsed.options.markup);
|
|
7232
|
+
if (markup) return { markup };
|
|
7233
|
+
const body = stringOpt(parsed.options.body);
|
|
7234
|
+
if (!body) throw new Error('needs --body "\u2026" (or --markup with @[Label](kind/id) mentions)');
|
|
7235
|
+
return { body };
|
|
7236
|
+
}
|
|
7237
|
+
async function discussGroups(ctx) {
|
|
7238
|
+
const result = await request(
|
|
7239
|
+
ctx,
|
|
7240
|
+
"GET",
|
|
7241
|
+
"/groups"
|
|
7242
|
+
);
|
|
7243
|
+
emit(ctx, result, () => {
|
|
7244
|
+
ctx.out.log("app name started");
|
|
7245
|
+
for (const group of result.groups) {
|
|
7246
|
+
ctx.out.log(`${group.appId} ${group.name} ${group.channelId ? "yes" : "no"}`);
|
|
7247
|
+
}
|
|
7248
|
+
});
|
|
7249
|
+
}
|
|
7250
|
+
async function discussList(ctx, parsed) {
|
|
7251
|
+
const query = new URLSearchParams();
|
|
7252
|
+
for (const [flag, param] of [
|
|
7253
|
+
["app", "app"],
|
|
7254
|
+
["q", "q"],
|
|
7255
|
+
["state", "state"],
|
|
7256
|
+
["limit", "limit"],
|
|
7257
|
+
["offset", "offset"]
|
|
7258
|
+
]) {
|
|
7259
|
+
const value = stringOpt(parsed.options[flag]);
|
|
7260
|
+
if (value) query.set(param, value);
|
|
7261
|
+
}
|
|
7262
|
+
const qs = query.toString();
|
|
7263
|
+
const page = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
|
|
7264
|
+
emit(ctx, page, () => {
|
|
7265
|
+
ctx.out.log(`topics \u2014 ${page.topics.length} of ${page.total}`);
|
|
7266
|
+
ctx.out.log("id state app replies subject");
|
|
7267
|
+
for (const topic of page.topics) {
|
|
7268
|
+
ctx.out.log(
|
|
7269
|
+
`${topic.id} ${state(topic)} ${topic.appId ?? ""} ${topic.replyCount} ${topic.subject}`
|
|
7270
|
+
);
|
|
7271
|
+
}
|
|
7272
|
+
});
|
|
7273
|
+
}
|
|
7274
|
+
async function discussRead(ctx, id) {
|
|
7275
|
+
const result = await request(
|
|
7276
|
+
ctx,
|
|
7277
|
+
"GET",
|
|
7278
|
+
`/topics/${encodeURIComponent(id)}`
|
|
7279
|
+
);
|
|
7280
|
+
emit(ctx, result, () => {
|
|
7281
|
+
ctx.out.log(`${result.topic.subject} [${state(result.topic)}] ${result.topic.appId ?? ""}`);
|
|
7282
|
+
for (const post of result.posts) {
|
|
7283
|
+
const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
|
|
7284
|
+
ctx.out.log(`
|
|
7285
|
+
\u2014 ${who}`);
|
|
7286
|
+
ctx.out.log(bodyWithRefs(post));
|
|
7287
|
+
for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
|
|
7288
|
+
}
|
|
7289
|
+
});
|
|
7290
|
+
}
|
|
7291
|
+
async function discussPost(ctx, parsed) {
|
|
7292
|
+
const appId = stringOpt(parsed.options.app);
|
|
7293
|
+
if (!appId) throw new Error("discuss post needs --app <appId>");
|
|
7294
|
+
const subject = stringOpt(parsed.options.subject);
|
|
7295
|
+
if (!subject) throw new Error('discuss post needs --subject "\u2026"');
|
|
7296
|
+
const created = await request(ctx, "POST", "/topics", {
|
|
7297
|
+
appId,
|
|
7298
|
+
subject,
|
|
7299
|
+
...content(parsed)
|
|
7300
|
+
});
|
|
7301
|
+
emit(ctx, created, () => ctx.out.log(`opened topic ${created.id}`));
|
|
7302
|
+
}
|
|
7303
|
+
async function discussReply(ctx, id, parsed) {
|
|
7304
|
+
const created = await request(
|
|
7305
|
+
ctx,
|
|
7306
|
+
"POST",
|
|
7307
|
+
`/topics/${encodeURIComponent(id)}/replies`,
|
|
7308
|
+
content(parsed)
|
|
7309
|
+
);
|
|
7310
|
+
emit(ctx, created, () => ctx.out.log(`replied ${created.id}`));
|
|
7311
|
+
}
|
|
7312
|
+
async function discussResolve(ctx, id, resolved) {
|
|
7313
|
+
const result = await request(
|
|
7314
|
+
ctx,
|
|
7315
|
+
"PATCH",
|
|
7316
|
+
`/topics/${encodeURIComponent(id)}`,
|
|
7317
|
+
{ resolved }
|
|
7318
|
+
);
|
|
7319
|
+
emit(ctx, result, () => ctx.out.log(`${resolved ? "resolved" : "reopened"} ${result.id}`));
|
|
7320
|
+
}
|
|
7321
|
+
async function discussWho(ctx, parsed) {
|
|
7322
|
+
const query = new URLSearchParams({ q: stringOpt(parsed.options.q) ?? "" });
|
|
7323
|
+
const app = stringOpt(parsed.options.app);
|
|
7324
|
+
if (app) query.set("app", app);
|
|
7325
|
+
const kinds = stringOpt(parsed.options.kinds);
|
|
7326
|
+
if (kinds) query.set("kinds", kinds);
|
|
7327
|
+
const found = await request(ctx, "GET", `/mentionables?${query.toString()}`);
|
|
7328
|
+
emit(ctx, found, () => {
|
|
7329
|
+
ctx.out.log("mention kind hint");
|
|
7330
|
+
for (const item of found.items) {
|
|
7331
|
+
ctx.out.log(`@[${item.label}](${item.kind}/${item.id}) ${item.kind} ${item.hint ?? ""}`);
|
|
7332
|
+
}
|
|
7333
|
+
if (found.failed.length > 0) ctx.out.error(`(no results from: ${found.failed.join(", ")})`);
|
|
7334
|
+
});
|
|
7335
|
+
}
|
|
7336
|
+
|
|
7337
|
+
// src/discuss-watch.ts
|
|
7338
|
+
var DEFAULT_INTERVAL_MS = 15e3;
|
|
7339
|
+
var DEFAULT_TIMEOUT_MS = 15 * 6e4;
|
|
7340
|
+
function numberOpt2(parsed, flag, fallback) {
|
|
7341
|
+
const raw = stringOpt(parsed.options[flag]);
|
|
7342
|
+
const value = raw == null ? NaN : Number(raw);
|
|
7343
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
7344
|
+
}
|
|
7345
|
+
async function get(ctx, path) {
|
|
7346
|
+
const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
|
|
7347
|
+
headers: { authorization: `Bearer ${ctx.token}` }
|
|
7348
|
+
});
|
|
7349
|
+
const data = await res.json().catch(() => ({}));
|
|
7350
|
+
if (!res.ok) throw new Error(`discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`);
|
|
7351
|
+
return data;
|
|
7352
|
+
}
|
|
7353
|
+
async function discussWatch(ctx, topicId, parsed) {
|
|
7354
|
+
const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
|
|
7355
|
+
const now = ctx.now ?? Date.now;
|
|
7356
|
+
const intervalMs = numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) * 1e3;
|
|
7357
|
+
const timeoutMs = numberOpt2(parsed, "timeout", DEFAULT_TIMEOUT_MS / 1e3) * 1e3;
|
|
7358
|
+
const by = stringOpt(parsed.options.by);
|
|
7359
|
+
const self = stringOpt(parsed.options.self);
|
|
7360
|
+
const deadline = now() + timeoutMs;
|
|
7361
|
+
const fresh = (post) => (!by || post.authorId === by) && (!self || post.authorId !== self);
|
|
7362
|
+
let seen = /* @__PURE__ */ new Set();
|
|
7363
|
+
let since = now();
|
|
7364
|
+
if (topicId) {
|
|
7365
|
+
const first = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
|
|
7366
|
+
seen = new Set(first.posts.map((post) => post.id));
|
|
7367
|
+
}
|
|
7368
|
+
for (; ; ) {
|
|
7369
|
+
if (topicId) {
|
|
7370
|
+
const { posts } = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
|
|
7371
|
+
const added = posts.filter((post) => !seen.has(post.id) && fresh(post));
|
|
7372
|
+
if (added.length > 0) return report2(ctx, { found: true, posts: added });
|
|
7373
|
+
for (const post of posts) seen.add(post.id);
|
|
7374
|
+
} else {
|
|
7375
|
+
const app = stringOpt(parsed.options.app);
|
|
7376
|
+
const { topics } = await get(
|
|
7377
|
+
ctx,
|
|
7378
|
+
`/topics?state=all${app ? `&app=${encodeURIComponent(app)}` : ""}`
|
|
7379
|
+
);
|
|
7380
|
+
const active = topics.filter((topic) => topic.lastActivityAt > since);
|
|
7381
|
+
if (active.length > 0) return report2(ctx, { found: true, topics: active });
|
|
7382
|
+
since = now();
|
|
7383
|
+
}
|
|
7384
|
+
if (now() >= deadline) return report2(ctx, { found: false });
|
|
7385
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
|
|
7386
|
+
}
|
|
7387
|
+
}
|
|
7388
|
+
function report2(ctx, result) {
|
|
7389
|
+
if (ctx.json) {
|
|
7390
|
+
ctx.out.log(JSON.stringify(result, null, 2));
|
|
7391
|
+
} else if (result.found) {
|
|
7392
|
+
for (const post of result.posts ?? []) {
|
|
7393
|
+
const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
|
|
7394
|
+
ctx.out.log(`\u2014 ${who}
|
|
7395
|
+
${post.body}`);
|
|
7396
|
+
}
|
|
7397
|
+
for (const topic of result.topics ?? []) {
|
|
7398
|
+
ctx.out.log(`${topic.id} ${topic.appId ?? ""} ${topic.subject}`);
|
|
7399
|
+
}
|
|
7400
|
+
}
|
|
7401
|
+
return result;
|
|
7402
|
+
}
|
|
7403
|
+
var WatchTimeoutError = class extends Error {
|
|
7404
|
+
code = "watch_timeout";
|
|
7405
|
+
constructor() {
|
|
7406
|
+
super("nothing new before the timeout");
|
|
7407
|
+
this.name = "WatchTimeoutError";
|
|
7408
|
+
}
|
|
7409
|
+
};
|
|
7410
|
+
|
|
7411
|
+
// src/discuss-command.ts
|
|
7412
|
+
var ALLOWED = [
|
|
7413
|
+
"config",
|
|
7414
|
+
"token",
|
|
7415
|
+
"email",
|
|
7416
|
+
"json",
|
|
7417
|
+
"app",
|
|
7418
|
+
"subject",
|
|
7419
|
+
"body",
|
|
7420
|
+
"markup",
|
|
7421
|
+
"state",
|
|
7422
|
+
"q",
|
|
7423
|
+
"kinds",
|
|
7424
|
+
"limit",
|
|
7425
|
+
"offset",
|
|
7426
|
+
"reopen",
|
|
7427
|
+
"by",
|
|
7428
|
+
"self",
|
|
7429
|
+
"interval",
|
|
7430
|
+
"timeout"
|
|
7431
|
+
];
|
|
7432
|
+
function requireId(id, action) {
|
|
7433
|
+
if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
|
|
7434
|
+
return id;
|
|
7435
|
+
}
|
|
7436
|
+
async function buildContext(parsed, deps) {
|
|
7437
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
7438
|
+
const doFetch = deps.fetch ?? fetch;
|
|
7439
|
+
const out = deps.stdout ?? console;
|
|
7440
|
+
const token = await getDeveloperToken(
|
|
7441
|
+
cfg,
|
|
7442
|
+
{
|
|
7443
|
+
configPath: cfg.configPath,
|
|
7444
|
+
token: stringOpt(parsed.options.token),
|
|
7445
|
+
email: stringOpt(parsed.options.email),
|
|
7446
|
+
open: false
|
|
7447
|
+
},
|
|
7448
|
+
doFetch,
|
|
7449
|
+
out
|
|
7450
|
+
);
|
|
7451
|
+
return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
|
|
7452
|
+
}
|
|
7453
|
+
async function discussCommand(parsed, deps = {}) {
|
|
7454
|
+
assertArgs(parsed, ALLOWED, 3);
|
|
7455
|
+
const action = parsed.positionals[1];
|
|
7456
|
+
const id = parsed.positionals[2];
|
|
7457
|
+
if (!action) throw new Error('"discuss" needs an action. Run "odla-ai help".');
|
|
7458
|
+
const ctx = await buildContext(parsed, deps);
|
|
7459
|
+
switch (action) {
|
|
7460
|
+
case "groups":
|
|
7461
|
+
return discussGroups(ctx);
|
|
7462
|
+
case "list":
|
|
7463
|
+
case "topics":
|
|
7464
|
+
return discussList(ctx, parsed);
|
|
7465
|
+
case "read":
|
|
7466
|
+
return discussRead(ctx, requireId(id, "read"));
|
|
7467
|
+
case "post":
|
|
7468
|
+
return discussPost(ctx, parsed);
|
|
7469
|
+
case "reply":
|
|
7470
|
+
return discussReply(ctx, requireId(id, "reply"), parsed);
|
|
7471
|
+
case "resolve":
|
|
7472
|
+
return discussResolve(ctx, requireId(id, "resolve"), parsed.options.reopen !== true);
|
|
7473
|
+
case "who":
|
|
7474
|
+
return discussWho(ctx, parsed);
|
|
7475
|
+
case "watch": {
|
|
7476
|
+
const result = await discussWatch(ctx, id, parsed);
|
|
7477
|
+
if (!result.found) throw new WatchTimeoutError();
|
|
7478
|
+
return;
|
|
7479
|
+
}
|
|
7480
|
+
default:
|
|
7481
|
+
throw new Error(`unknown discuss action "${action}". Run "odla-ai help".`);
|
|
7482
|
+
}
|
|
7483
|
+
}
|
|
7484
|
+
|
|
7187
7485
|
// src/pm-actions.ts
|
|
7188
7486
|
var FIELD_MAP = {
|
|
7189
7487
|
title: { key: "title" },
|
|
@@ -7247,7 +7545,7 @@ function statusCol(entity, r) {
|
|
|
7247
7545
|
function printRecord(ctx, entity, r) {
|
|
7248
7546
|
ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
|
|
7249
7547
|
}
|
|
7250
|
-
function
|
|
7548
|
+
function emit2(ctx, value, human) {
|
|
7251
7549
|
if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
|
|
7252
7550
|
else human();
|
|
7253
7551
|
}
|
|
@@ -7273,7 +7571,7 @@ async function pmList(ctx, entity, parsed) {
|
|
|
7273
7571
|
}
|
|
7274
7572
|
const qs = q.toString();
|
|
7275
7573
|
const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
|
|
7276
|
-
|
|
7574
|
+
emit2(ctx, page, () => {
|
|
7277
7575
|
ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
|
|
7278
7576
|
ctx.out.log("id state app title");
|
|
7279
7577
|
for (const r of page.records) printRecord(ctx, entity, r);
|
|
@@ -7287,25 +7585,25 @@ async function pmAdd(ctx, entity, parsed) {
|
|
|
7287
7585
|
if (entity === "bug" && !input.description)
|
|
7288
7586
|
throw new Error("pm bug add needs --description <context> (or --body <context>)");
|
|
7289
7587
|
const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
|
|
7290
|
-
|
|
7588
|
+
emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
|
|
7291
7589
|
}
|
|
7292
7590
|
async function pmGet(ctx, entity, id) {
|
|
7293
7591
|
const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
|
|
7294
|
-
|
|
7592
|
+
emit2(ctx, record5, () => printRecord(ctx, entity, record5));
|
|
7295
7593
|
}
|
|
7296
7594
|
async function pmSet(ctx, entity, id, parsed) {
|
|
7297
7595
|
const patch2 = collectEntityFields(entity, parsed, true);
|
|
7298
7596
|
if (Object.keys(patch2).length === 0)
|
|
7299
7597
|
throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
|
|
7300
7598
|
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
7301
|
-
|
|
7599
|
+
emit2(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
|
|
7302
7600
|
}
|
|
7303
7601
|
async function pmDone(ctx, entity, id, parsed) {
|
|
7304
7602
|
const decisionId = stringOpt(parsed.options.decision);
|
|
7305
7603
|
if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
|
|
7306
7604
|
const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
|
|
7307
7605
|
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
7308
|
-
|
|
7606
|
+
emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
|
|
7309
7607
|
}
|
|
7310
7608
|
async function allRecords(ctx, entity, appId) {
|
|
7311
7609
|
const records = [];
|
|
@@ -7338,7 +7636,7 @@ async function pmHandoff(ctx, parsed) {
|
|
|
7338
7636
|
...handoff,
|
|
7339
7637
|
clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
|
|
7340
7638
|
};
|
|
7341
|
-
|
|
7639
|
+
emit2(ctx, result, () => {
|
|
7342
7640
|
if (result.clean) {
|
|
7343
7641
|
ctx.out.log(`${appId}: no unresolved PM work`);
|
|
7344
7642
|
return;
|
|
@@ -7371,7 +7669,7 @@ async function pmComments(ctx, entity, id) {
|
|
|
7371
7669
|
"GET",
|
|
7372
7670
|
`/${entity}/${encodeURIComponent(id)}/comments`
|
|
7373
7671
|
);
|
|
7374
|
-
|
|
7672
|
+
emit2(ctx, messages, () => {
|
|
7375
7673
|
if (messages.length === 0) ctx.out.log("(no comments)");
|
|
7376
7674
|
else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
|
|
7377
7675
|
});
|
|
@@ -7390,7 +7688,7 @@ var ALIASES = {
|
|
|
7390
7688
|
decision: "decision",
|
|
7391
7689
|
bug: "bug"
|
|
7392
7690
|
};
|
|
7393
|
-
var
|
|
7691
|
+
var ALLOWED2 = [
|
|
7394
7692
|
"config",
|
|
7395
7693
|
"token",
|
|
7396
7694
|
"email",
|
|
@@ -7414,11 +7712,11 @@ var ALLOWED = [
|
|
|
7414
7712
|
"offset",
|
|
7415
7713
|
"q"
|
|
7416
7714
|
];
|
|
7417
|
-
function
|
|
7715
|
+
function requireId2(id, action) {
|
|
7418
7716
|
if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
|
|
7419
7717
|
return id;
|
|
7420
7718
|
}
|
|
7421
|
-
async function
|
|
7719
|
+
async function buildContext2(parsed, deps) {
|
|
7422
7720
|
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
7423
7721
|
const doFetch = deps.fetch ?? fetch;
|
|
7424
7722
|
const out = deps.stdout ?? console;
|
|
@@ -7433,14 +7731,14 @@ async function buildContext(parsed, deps) {
|
|
|
7433
7731
|
async function pmCommand(parsed, deps = {}) {
|
|
7434
7732
|
const word = parsed.positionals[1] ?? "";
|
|
7435
7733
|
if (word === "handoff") {
|
|
7436
|
-
assertArgs(parsed,
|
|
7437
|
-
return pmHandoff(await
|
|
7734
|
+
assertArgs(parsed, ALLOWED2, 2);
|
|
7735
|
+
return pmHandoff(await buildContext2(parsed, deps), parsed);
|
|
7438
7736
|
}
|
|
7439
7737
|
const entity = ALIASES[word];
|
|
7440
7738
|
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
7441
7739
|
const action = parsed.positionals[2] ?? "list";
|
|
7442
|
-
assertArgs(parsed,
|
|
7443
|
-
const ctx = await
|
|
7740
|
+
assertArgs(parsed, ALLOWED2, 4);
|
|
7741
|
+
const ctx = await buildContext2(parsed, deps);
|
|
7444
7742
|
const id = parsed.positionals[3];
|
|
7445
7743
|
switch (action) {
|
|
7446
7744
|
case "list":
|
|
@@ -7449,21 +7747,21 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
7449
7747
|
case "create":
|
|
7450
7748
|
return pmAdd(ctx, entity, parsed);
|
|
7451
7749
|
case "get":
|
|
7452
|
-
return pmGet(ctx, entity,
|
|
7750
|
+
return pmGet(ctx, entity, requireId2(id, action));
|
|
7453
7751
|
case "set":
|
|
7454
7752
|
case "update":
|
|
7455
7753
|
case "status":
|
|
7456
7754
|
case "move":
|
|
7457
|
-
return pmSet(ctx, entity,
|
|
7755
|
+
return pmSet(ctx, entity, requireId2(id, action), parsed);
|
|
7458
7756
|
case "done":
|
|
7459
|
-
return pmDone(ctx, entity,
|
|
7757
|
+
return pmDone(ctx, entity, requireId2(id, action), parsed);
|
|
7460
7758
|
case "comment":
|
|
7461
|
-
return pmComment(ctx, entity,
|
|
7759
|
+
return pmComment(ctx, entity, requireId2(id, action), parsed);
|
|
7462
7760
|
case "comments":
|
|
7463
|
-
return pmComments(ctx, entity,
|
|
7761
|
+
return pmComments(ctx, entity, requireId2(id, action));
|
|
7464
7762
|
case "rm":
|
|
7465
7763
|
case "delete":
|
|
7466
|
-
return pmRemove(ctx, entity,
|
|
7764
|
+
return pmRemove(ctx, entity, requireId2(id, action));
|
|
7467
7765
|
default:
|
|
7468
7766
|
throw new Error(`unknown pm action "${action}". Try list|add|get|set|done|comment|comments|rm.`);
|
|
7469
7767
|
}
|
|
@@ -7929,6 +8227,18 @@ var COMMAND_SURFACE = {
|
|
|
7929
8227
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
7930
8228
|
capabilities: {},
|
|
7931
8229
|
code: { connect: {} },
|
|
8230
|
+
// `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
|
|
8231
|
+
discuss: {
|
|
8232
|
+
groups: {},
|
|
8233
|
+
list: {},
|
|
8234
|
+
topics: {},
|
|
8235
|
+
read: {},
|
|
8236
|
+
post: {},
|
|
8237
|
+
reply: {},
|
|
8238
|
+
resolve: {},
|
|
8239
|
+
who: {},
|
|
8240
|
+
watch: {}
|
|
8241
|
+
},
|
|
7932
8242
|
doctor: {},
|
|
7933
8243
|
help: {},
|
|
7934
8244
|
init: {},
|
|
@@ -8516,7 +8826,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
8516
8826
|
return out;
|
|
8517
8827
|
}
|
|
8518
8828
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
8519
|
-
function
|
|
8829
|
+
function report3(ctx, impacts) {
|
|
8520
8830
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
8521
8831
|
ctx.out.log(
|
|
8522
8832
|
`${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.`
|
|
@@ -8554,7 +8864,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
8554
8864
|
}
|
|
8555
8865
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
8556
8866
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
8557
|
-
|
|
8867
|
+
report3(ctx, impacts);
|
|
8558
8868
|
}
|
|
8559
8869
|
|
|
8560
8870
|
// src/runbook-lint.ts
|
|
@@ -8787,7 +9097,7 @@ async function whoamiCommand(parsed, deps = {}) {
|
|
|
8787
9097
|
}
|
|
8788
9098
|
|
|
8789
9099
|
// src/runbook-command.ts
|
|
8790
|
-
var
|
|
9100
|
+
var ALLOWED3 = [
|
|
8791
9101
|
"config",
|
|
8792
9102
|
"token",
|
|
8793
9103
|
"email",
|
|
@@ -8827,7 +9137,7 @@ async function loadOrDefaultConfig(configPath, action, appId) {
|
|
|
8827
9137
|
}
|
|
8828
9138
|
return loadProjectConfig(configPath);
|
|
8829
9139
|
}
|
|
8830
|
-
async function
|
|
9140
|
+
async function buildContext3(parsed, deps, action) {
|
|
8831
9141
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
8832
9142
|
const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
|
|
8833
9143
|
const doFetch = deps.fetch ?? fetch;
|
|
@@ -8883,8 +9193,8 @@ async function buildContext2(parsed, deps, action) {
|
|
|
8883
9193
|
}
|
|
8884
9194
|
async function runbookCommand(parsed, deps = {}) {
|
|
8885
9195
|
const action = parsed.positionals[1] ?? "list";
|
|
8886
|
-
assertArgs(parsed,
|
|
8887
|
-
const ctx = await
|
|
9196
|
+
assertArgs(parsed, ALLOWED3, action === "ask" || action === "search" ? 64 : 4);
|
|
9197
|
+
const ctx = await buildContext3(parsed, deps, action);
|
|
8888
9198
|
const slug = parsed.positionals[2];
|
|
8889
9199
|
switch (action) {
|
|
8890
9200
|
case "list":
|
|
@@ -9075,31 +9385,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
9075
9385
|
url.searchParams.set("job", job.jobId);
|
|
9076
9386
|
out.log(` Studio: ${url.toString()}`);
|
|
9077
9387
|
}
|
|
9078
|
-
function printHostedReport(out,
|
|
9079
|
-
out.log(`security report ${
|
|
9080
|
-
out.log(` coverage: ${
|
|
9081
|
-
out.log(` findings: confirmed=${
|
|
9082
|
-
out.log(` discovery: ${
|
|
9083
|
-
out.log(` validation: ${
|
|
9084
|
-
for (const finding of
|
|
9388
|
+
function printHostedReport(out, report4) {
|
|
9389
|
+
out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
|
|
9390
|
+
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}`);
|
|
9391
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
|
|
9392
|
+
out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
|
|
9393
|
+
out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
|
|
9394
|
+
for (const finding of report4.findings) {
|
|
9085
9395
|
const location = finding.locations[0];
|
|
9086
9396
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
9087
9397
|
}
|
|
9088
|
-
for (const limitation of
|
|
9398
|
+
for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
|
|
9089
9399
|
}
|
|
9090
|
-
function enforceHostedReportGate(
|
|
9400
|
+
function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
|
|
9091
9401
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
9092
9402
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
9093
9403
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
9094
9404
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
9095
|
-
const confirmed =
|
|
9096
|
-
const leads = failOnCandidates ?
|
|
9097
|
-
const incomplete =
|
|
9405
|
+
const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
9406
|
+
const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
9407
|
+
const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
9098
9408
|
if (confirmed.length || leads.length || incomplete) {
|
|
9099
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
9409
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
|
|
9100
9410
|
}
|
|
9101
9411
|
if (emitSuccess) {
|
|
9102
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
9412
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
|
|
9103
9413
|
}
|
|
9104
9414
|
}
|
|
9105
9415
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -9182,17 +9492,17 @@ async function runHostedSecurity(options) {
|
|
|
9182
9492
|
allowNetwork: false
|
|
9183
9493
|
}
|
|
9184
9494
|
});
|
|
9185
|
-
const
|
|
9186
|
-
await (0, import_node3.writeSecurityArtifacts)(output,
|
|
9187
|
-
const reportDigest = await (0, import_security.securityFingerprint)(
|
|
9495
|
+
const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
9496
|
+
await (0, import_node3.writeSecurityArtifacts)(output, report4);
|
|
9497
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report4);
|
|
9188
9498
|
await hosted.complete({
|
|
9189
9499
|
reportDigest,
|
|
9190
|
-
coverageStatus:
|
|
9191
|
-
confirmed:
|
|
9192
|
-
candidates:
|
|
9500
|
+
coverageStatus: report4.coverageStatus,
|
|
9501
|
+
confirmed: report4.metrics.confirmed,
|
|
9502
|
+
candidates: report4.metrics.candidates
|
|
9193
9503
|
}, { signal: options.signal });
|
|
9194
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
9195
|
-
return Object.freeze({ report:
|
|
9504
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
|
|
9505
|
+
return Object.freeze({ report: report4, run: hosted.run, output });
|
|
9196
9506
|
}
|
|
9197
9507
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
9198
9508
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -9202,11 +9512,11 @@ function selectEnv(requested, declared, configPath, rootDir) {
|
|
|
9202
9512
|
}
|
|
9203
9513
|
return env;
|
|
9204
9514
|
}
|
|
9205
|
-
async function injectedToken(options,
|
|
9206
|
-
const value = options.token ?? await options.getToken?.(Object.freeze({ ...
|
|
9515
|
+
async function injectedToken(options, request2) {
|
|
9516
|
+
const value = options.token ?? await options.getToken?.(Object.freeze({ ...request2 }));
|
|
9207
9517
|
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
9208
9518
|
throw new Error(
|
|
9209
|
-
|
|
9519
|
+
request2.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
|
|
9210
9520
|
);
|
|
9211
9521
|
}
|
|
9212
9522
|
return value;
|
|
@@ -9218,14 +9528,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
9218
9528
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
9219
9529
|
return { ...profile, maxHuntTasks };
|
|
9220
9530
|
}
|
|
9221
|
-
function printSummary(out, appId, env, run,
|
|
9222
|
-
const complete =
|
|
9531
|
+
function printSummary(out, appId, env, run, report4, output) {
|
|
9532
|
+
const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
|
|
9223
9533
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
9224
9534
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
9225
9535
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
9226
|
-
out.log(` coverage: ${
|
|
9227
|
-
if (
|
|
9228
|
-
out.log(` findings: confirmed=${
|
|
9536
|
+
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}`);
|
|
9537
|
+
if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
|
|
9538
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
|
|
9229
9539
|
out.log(` report: ${(0, import_node_path15.resolve)(output, "REPORT.md")}`);
|
|
9230
9540
|
}
|
|
9231
9541
|
function formatBudget(usage) {
|
|
@@ -9461,13 +9771,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
9461
9771
|
}
|
|
9462
9772
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
9463
9773
|
}
|
|
9464
|
-
const
|
|
9774
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
9465
9775
|
if (parsed.options.json === true) {
|
|
9466
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
9776
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
|
|
9467
9777
|
} else {
|
|
9468
|
-
printHostedReport(context.stdout,
|
|
9778
|
+
printHostedReport(context.stdout, report4);
|
|
9469
9779
|
}
|
|
9470
|
-
enforceHostedReportGate(
|
|
9780
|
+
enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
|
|
9471
9781
|
}
|
|
9472
9782
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
9473
9783
|
if (parsed.options.source === true) {
|
|
@@ -9508,11 +9818,11 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9508
9818
|
sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
|
|
9509
9819
|
fetch: doFetch,
|
|
9510
9820
|
stdout: out,
|
|
9511
|
-
getToken: async (
|
|
9512
|
-
if (
|
|
9821
|
+
getToken: async (request2) => {
|
|
9822
|
+
if (request2.scope === "platform:security:self") {
|
|
9513
9823
|
return getScopedPlatformToken({
|
|
9514
|
-
platform:
|
|
9515
|
-
scope:
|
|
9824
|
+
platform: request2.platform,
|
|
9825
|
+
scope: request2.scope,
|
|
9516
9826
|
email: stringOpt(parsed.options.email),
|
|
9517
9827
|
open,
|
|
9518
9828
|
fetch: doFetch,
|
|
@@ -9521,7 +9831,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9521
9831
|
});
|
|
9522
9832
|
}
|
|
9523
9833
|
const cfg = await loadProjectConfig(configPath);
|
|
9524
|
-
if (platformAudience(cfg.platformUrl) !== platformAudience(
|
|
9834
|
+
if (platformAudience(cfg.platformUrl) !== platformAudience(request2.platform)) {
|
|
9525
9835
|
throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
|
|
9526
9836
|
}
|
|
9527
9837
|
return getDeveloperToken(
|
|
@@ -9534,13 +9844,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9534
9844
|
});
|
|
9535
9845
|
enforceLocalGate(result.report, parsed);
|
|
9536
9846
|
}
|
|
9537
|
-
function enforceLocalGate(
|
|
9847
|
+
function enforceLocalGate(report4, parsed) {
|
|
9538
9848
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
9539
9849
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
9540
9850
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
9541
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
9542
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
9543
|
-
const incomplete =
|
|
9851
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(report4, failOn);
|
|
9852
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
9853
|
+
const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
9544
9854
|
if (confirmed.length || leads.length || incomplete) {
|
|
9545
9855
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
9546
9856
|
}
|
|
@@ -9579,9 +9889,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
9579
9889
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
9580
9890
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
9581
9891
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
9582
|
-
const
|
|
9583
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
9584
|
-
else printHostedReport(context.stdout,
|
|
9892
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId });
|
|
9893
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
|
|
9894
|
+
else printHostedReport(context.stdout, report4);
|
|
9585
9895
|
return;
|
|
9586
9896
|
}
|
|
9587
9897
|
if (sub !== "run") {
|
|
@@ -9665,7 +9975,8 @@ async function securityStatus(parsed, dependencies) {
|
|
|
9665
9975
|
|
|
9666
9976
|
// src/cli.ts
|
|
9667
9977
|
function exitCodeFor(err) {
|
|
9668
|
-
|
|
9978
|
+
const code = err?.code;
|
|
9979
|
+
return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
|
|
9669
9980
|
}
|
|
9670
9981
|
async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
9671
9982
|
const parsed = parseArgv(argv);
|
|
@@ -9713,6 +10024,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
9713
10024
|
await pmCommand(parsed, dependencies);
|
|
9714
10025
|
return;
|
|
9715
10026
|
}
|
|
10027
|
+
if (command === "discuss") {
|
|
10028
|
+
await discussCommand(parsed, dependencies);
|
|
10029
|
+
return;
|
|
10030
|
+
}
|
|
9716
10031
|
if (command === "provision") {
|
|
9717
10032
|
await provisionCommand(parsed, dependencies);
|
|
9718
10033
|
return;
|