@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/index.cjs
CHANGED
|
@@ -613,23 +613,23 @@ function adminAiAuditQuery(filters) {
|
|
|
613
613
|
}
|
|
614
614
|
return `?limit=${filters.limit}`;
|
|
615
615
|
}
|
|
616
|
-
async function readAdminAiAudit(
|
|
617
|
-
const response2 = await
|
|
618
|
-
headers:
|
|
616
|
+
async function readAdminAiAudit(request2) {
|
|
617
|
+
const response2 = await request2.fetch(`${request2.platform}/registry/platform/ai-audit${request2.query}`, {
|
|
618
|
+
headers: request2.headers
|
|
619
619
|
});
|
|
620
620
|
const body = await responseBody(response2);
|
|
621
621
|
if (!response2.ok) throw new Error(apiError(response2.status, body));
|
|
622
|
-
if (
|
|
623
|
-
|
|
622
|
+
if (request2.json) {
|
|
623
|
+
request2.stdout.log(JSON.stringify(body, null, 2));
|
|
624
624
|
return;
|
|
625
625
|
}
|
|
626
626
|
const events = isRecord(body) && Array.isArray(body.events) ? body.events.filter(isRecord) : [];
|
|
627
|
-
|
|
627
|
+
request2.stdout.log("when change target before -> after actor");
|
|
628
628
|
for (const event of events) {
|
|
629
629
|
const before = isRecord(event.oldPolicy) ? event.oldPolicy : void 0;
|
|
630
630
|
const after = isRecord(event.newPolicy) ? event.newPolicy : void 0;
|
|
631
631
|
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";
|
|
632
|
-
|
|
632
|
+
request2.stdout.log([
|
|
633
633
|
timestamp(event.createdAt),
|
|
634
634
|
String(event.changeKind ?? ""),
|
|
635
635
|
String(event.purpose ?? event.provider ?? ""),
|
|
@@ -685,14 +685,14 @@ function adminAiUsageQuery(filters) {
|
|
|
685
685
|
const query = params.toString();
|
|
686
686
|
return query ? `?${query}` : "";
|
|
687
687
|
}
|
|
688
|
-
async function readAdminAiUsage(
|
|
689
|
-
const res = await
|
|
690
|
-
headers:
|
|
688
|
+
async function readAdminAiUsage(request2) {
|
|
689
|
+
const res = await request2.fetch(`${request2.platform}/registry/platform/ai-usage${request2.query}`, {
|
|
690
|
+
headers: request2.headers
|
|
691
691
|
});
|
|
692
692
|
const body = await responseBody2(res);
|
|
693
693
|
if (!res.ok) throw new Error(apiError2("read platform AI usage", res.status, body));
|
|
694
|
-
if (
|
|
695
|
-
else printUsage(body,
|
|
694
|
+
if (request2.json) request2.stdout.log(JSON.stringify(body, null, 2));
|
|
695
|
+
else printUsage(body, request2.stdout);
|
|
696
696
|
}
|
|
697
697
|
function usageLimit(value) {
|
|
698
698
|
if (!Number.isSafeInteger(value) || value < 1 || value > 500) {
|
|
@@ -3154,10 +3154,10 @@ function installSkill(options = {}) {
|
|
|
3154
3154
|
current.add(target);
|
|
3155
3155
|
targets.set(harness, current);
|
|
3156
3156
|
};
|
|
3157
|
-
const plan = (target,
|
|
3157
|
+
const plan = (target, content2, managedMerge = false, boundary = root) => {
|
|
3158
3158
|
const existing = plans.get(target);
|
|
3159
|
-
if (existing && existing.content !==
|
|
3160
|
-
plans.set(target, { target, content, boundary, managedMerge });
|
|
3159
|
+
if (existing && existing.content !== content2) throw new Error(`internal agent setup conflict for ${target}`);
|
|
3160
|
+
plans.set(target, { target, content: content2, boundary, managedMerge });
|
|
3161
3161
|
};
|
|
3162
3162
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
3163
3163
|
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);
|
|
@@ -3943,11 +3943,11 @@ async function gitBlobs(cwd, entries, maxBytes) {
|
|
|
3943
3943
|
if (!Number.isSafeInteger(length) || length < 0 || newline + 1 + length >= output.length) {
|
|
3944
3944
|
throw new Error("git object output has an invalid length");
|
|
3945
3945
|
}
|
|
3946
|
-
const
|
|
3946
|
+
const content2 = output.subarray(newline + 1, newline + 1 + length);
|
|
3947
3947
|
if (output[newline + 1 + length] !== 10) throw new Error("git object output is malformed");
|
|
3948
|
-
total +=
|
|
3948
|
+
total += content2.byteLength;
|
|
3949
3949
|
if (total > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
3950
|
-
blobs.push(
|
|
3950
|
+
blobs.push(content2);
|
|
3951
3951
|
offset = newline + 1 + length + 1;
|
|
3952
3952
|
}
|
|
3953
3953
|
return blobs;
|
|
@@ -3970,13 +3970,13 @@ async function materializeGitTree(source, commitSha, options = {}) {
|
|
|
3970
3970
|
try {
|
|
3971
3971
|
const blobs = await gitBlobs(sourceDir, entries, maxBytes);
|
|
3972
3972
|
for (const [index, entry] of entries.entries()) {
|
|
3973
|
-
const
|
|
3974
|
-
byteCount +=
|
|
3973
|
+
const content2 = blobs[index];
|
|
3974
|
+
byteCount += content2.byteLength;
|
|
3975
3975
|
if (byteCount > maxBytes) throw new Error(`workspace exceeds ${maxBytes} bytes`);
|
|
3976
3976
|
const target = (0, import_path2.resolve)(targetRoot, entry.path);
|
|
3977
3977
|
if (!target.startsWith(`${(0, import_path2.resolve)(targetRoot)}${import_path2.sep}`)) throw new TypeError("Git tree path escapes workspace");
|
|
3978
3978
|
await (0, import_promises3.mkdir)((0, import_path2.resolve)(target, ".."), { recursive: true });
|
|
3979
|
-
await (0, import_promises3.writeFile)(target,
|
|
3979
|
+
await (0, import_promises3.writeFile)(target, content2, { flag: "wx", mode: entry.mode === "100755" ? 493 : 420 });
|
|
3980
3980
|
}
|
|
3981
3981
|
return {
|
|
3982
3982
|
root,
|
|
@@ -4354,11 +4354,11 @@ var ID2 = /^[A-Za-z0-9._:-]{1,180}$/;
|
|
|
4354
4354
|
var MAX_PATCH_BYTES = 256 * 1024;
|
|
4355
4355
|
var MAX_STATE_BYTES = 64 * 1024;
|
|
4356
4356
|
async function createCodePortableCheckpoint(input) {
|
|
4357
|
-
const
|
|
4357
|
+
const state2 = normalizeState(input.state);
|
|
4358
4358
|
validateBaseAndPatch(input.baseCommitSha, input.patch);
|
|
4359
4359
|
const patchDigest = `sha256:${await sha256Hex(input.patch)}`;
|
|
4360
|
-
const stateDigest = `sha256:${await sha256Hex(canonicalJson(
|
|
4361
|
-
const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state, stateDigest };
|
|
4360
|
+
const stateDigest = `sha256:${await sha256Hex(canonicalJson(state2))}`;
|
|
4361
|
+
const fields = { schemaVersion: 1, baseCommitSha: input.baseCommitSha, patchDigest, state: state2, stateDigest };
|
|
4362
4362
|
const checkpointDigest = `sha256:${await sha256Hex(canonicalJson(fields))}`;
|
|
4363
4363
|
return freeze({ ...fields, patch: input.patch, checkpointDigest });
|
|
4364
4364
|
}
|
|
@@ -4379,8 +4379,8 @@ async function verifyCodePortableCheckpoint(value) {
|
|
|
4379
4379
|
return created;
|
|
4380
4380
|
}
|
|
4381
4381
|
function normalizeState(value) {
|
|
4382
|
-
const
|
|
4383
|
-
exact2(
|
|
4382
|
+
const state2 = object(value, "state");
|
|
4383
|
+
exact2(state2, [
|
|
4384
4384
|
"planCursor",
|
|
4385
4385
|
"conversationRefs",
|
|
4386
4386
|
"planningInputDigest",
|
|
@@ -4392,13 +4392,13 @@ function normalizeState(value) {
|
|
|
4392
4392
|
"unresolvedApprovals",
|
|
4393
4393
|
"trustStatus"
|
|
4394
4394
|
]);
|
|
4395
|
-
const planCursor =
|
|
4396
|
-
const conversations = strings(
|
|
4397
|
-
const approvals = strings(
|
|
4398
|
-
if (planCursor !== null && (typeof planCursor !== "string" || !ID2.test(planCursor)) || typeof
|
|
4395
|
+
const planCursor = state2.planCursor;
|
|
4396
|
+
const conversations = strings(state2.conversationRefs, "conversationRefs", ID2, 256, false);
|
|
4397
|
+
const approvals = strings(state2.unresolvedApprovals, "unresolvedApprovals", DIGEST2, 256, true);
|
|
4398
|
+
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) {
|
|
4399
4399
|
throw invalid2("Portable checkpoint state is malformed or outside its bounds.");
|
|
4400
4400
|
}
|
|
4401
|
-
const effects =
|
|
4401
|
+
const effects = state2.completedEffects.map((item) => {
|
|
4402
4402
|
const effect = object(item, "completed effect");
|
|
4403
4403
|
exact2(effect, ["effectId", "actionDigest", "receiptDigest"]);
|
|
4404
4404
|
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)) {
|
|
@@ -4413,18 +4413,18 @@ function normalizeState(value) {
|
|
|
4413
4413
|
if (new Set(effects.map((item) => item.effectId)).size !== effects.length) {
|
|
4414
4414
|
throw invalid2("Portable checkpoint repeats a completed effect.");
|
|
4415
4415
|
}
|
|
4416
|
-
const trustStatus =
|
|
4417
|
-
if (trustStatus === "candidate_untrusted" && (
|
|
4416
|
+
const trustStatus = state2.trustStatus;
|
|
4417
|
+
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)) {
|
|
4418
4418
|
throw invalid2("Portable checkpoint trust status does not match its evidence digests.");
|
|
4419
4419
|
}
|
|
4420
4420
|
const normalized = {
|
|
4421
4421
|
planCursor,
|
|
4422
4422
|
conversationRefs: conversations,
|
|
4423
|
-
planningInputDigest:
|
|
4424
|
-
buildPolicyDigest:
|
|
4425
|
-
dependencyLayerDigest:
|
|
4426
|
-
verificationDigest:
|
|
4427
|
-
reviewDigest:
|
|
4423
|
+
planningInputDigest: state2.planningInputDigest,
|
|
4424
|
+
buildPolicyDigest: state2.buildPolicyDigest,
|
|
4425
|
+
dependencyLayerDigest: state2.dependencyLayerDigest,
|
|
4426
|
+
verificationDigest: state2.verificationDigest,
|
|
4427
|
+
reviewDigest: state2.reviewDigest,
|
|
4428
4428
|
completedEffects: effects,
|
|
4429
4429
|
unresolvedApprovals: approvals,
|
|
4430
4430
|
trustStatus
|
|
@@ -4541,60 +4541,60 @@ async function createConversionRegistry(config) {
|
|
|
4541
4541
|
policies.set(policy.conversionId, Object.freeze(policy));
|
|
4542
4542
|
}
|
|
4543
4543
|
const outputCounts = /* @__PURE__ */ new Map();
|
|
4544
|
-
const
|
|
4544
|
+
const get2 = (id, kind) => {
|
|
4545
4545
|
const policy = policies.get(id);
|
|
4546
4546
|
if (!policy || policy.output.kind !== kind) throw new CamelError("conversion_rejected", "Conversion policy is missing or has the wrong output kind.");
|
|
4547
4547
|
return policy;
|
|
4548
4548
|
};
|
|
4549
4549
|
const checked = (source, id, kind) => {
|
|
4550
|
-
const policy =
|
|
4550
|
+
const policy = get2(id, kind);
|
|
4551
4551
|
if (utf8Length(source.value) > policy.maximumSourceBytes) throw new CamelError("limit_exceeded", "Unsafe conversion input exceeds its byte bound.");
|
|
4552
4552
|
return policy;
|
|
4553
4553
|
};
|
|
4554
|
-
const
|
|
4554
|
+
const emit3 = (source, policy, value) => convert(source, policy, value, outputCounts);
|
|
4555
4555
|
const operations = Object.freeze({
|
|
4556
4556
|
boolean: async (value, id) => {
|
|
4557
4557
|
const policy = checked(value, id, "boolean");
|
|
4558
|
-
return
|
|
4558
|
+
return emit3(value, policy, requireBoolean(value.value));
|
|
4559
4559
|
},
|
|
4560
4560
|
integer: async (value, id) => {
|
|
4561
4561
|
const policy = checked(value, id, "integer");
|
|
4562
|
-
return
|
|
4562
|
+
return emit3(value, policy, boundedInteger(value.value, policy.output));
|
|
4563
4563
|
},
|
|
4564
4564
|
finiteNumber: async (value, id) => {
|
|
4565
4565
|
const policy = checked(value, id, "finite_number");
|
|
4566
|
-
return
|
|
4566
|
+
return emit3(value, policy, boundedNumber(value.value, policy.output));
|
|
4567
4567
|
},
|
|
4568
4568
|
enum: async (value, id) => {
|
|
4569
4569
|
const policy = checked(value, id, "enum");
|
|
4570
|
-
return
|
|
4570
|
+
return emit3(value, policy, enumMember(value.value, policy.output));
|
|
4571
4571
|
},
|
|
4572
4572
|
date: async (value, id) => {
|
|
4573
4573
|
const policy = checked(value, id, "date");
|
|
4574
|
-
return
|
|
4574
|
+
return emit3(value, policy, canonicalDate(value.value, policy.output));
|
|
4575
4575
|
},
|
|
4576
4576
|
registeredId: async (value, id) => {
|
|
4577
4577
|
const policy = checked(value, id, "registered_id");
|
|
4578
4578
|
const registry = config.registeredIds?.[policy.output.registryId];
|
|
4579
4579
|
const output = typeof value.value === "string" ? registry?.values[value.value] : void 0;
|
|
4580
4580
|
if (!output) throw new CamelError("conversion_rejected", "Registered-ID conversion rejected the candidate.");
|
|
4581
|
-
return
|
|
4581
|
+
return emit3(value, policy, output);
|
|
4582
4582
|
},
|
|
4583
4583
|
digest: async (value, id) => {
|
|
4584
4584
|
const policy = checked(value, id, "digest");
|
|
4585
4585
|
if (!(value.value instanceof Uint8Array)) throw new CamelError("conversion_rejected", "Digest conversion requires bytes.");
|
|
4586
|
-
return
|
|
4586
|
+
return emit3(value, policy, await sha256Hex(value.value));
|
|
4587
4587
|
},
|
|
4588
4588
|
measure: async (value, metric, id) => {
|
|
4589
4589
|
const policy = checked(value, id, "integer");
|
|
4590
4590
|
const measured = measure(value.value, metric);
|
|
4591
|
-
return
|
|
4591
|
+
return emit3(value, policy, boundedInteger(measured, policy.output));
|
|
4592
4592
|
},
|
|
4593
4593
|
test: async (value, predicateId, id) => {
|
|
4594
4594
|
const policy = checked(value, id, "boolean");
|
|
4595
4595
|
const predicate = config.predicates?.[predicateId];
|
|
4596
4596
|
if (!predicate) throw new CamelError("conversion_rejected", "Predicate is not registered.");
|
|
4597
|
-
return
|
|
4597
|
+
return emit3(value, policy, evaluatePredicate(value.value, predicate, config.registeredIds));
|
|
4598
4598
|
}
|
|
4599
4599
|
});
|
|
4600
4600
|
return Object.freeze({ operations, policy: (id) => policies.get(id) ?? missingPolicy() });
|
|
@@ -4813,11 +4813,11 @@ async function digestStagedWorkspace(root, limits) {
|
|
|
4813
4813
|
const hash = (0, import_crypto.createHash)("sha256");
|
|
4814
4814
|
let bytes = 0;
|
|
4815
4815
|
for (const file of files.sort((left, right) => left.path.localeCompare(right.path))) {
|
|
4816
|
-
const
|
|
4817
|
-
bytes += Buffer.byteLength(file.path) +
|
|
4816
|
+
const content2 = await (0, import_promises5.readFile)(file.target);
|
|
4817
|
+
bytes += Buffer.byteLength(file.path) + content2.byteLength;
|
|
4818
4818
|
if (bytes > limits.maxBytes) throw new TypeError("workspace digest exceeds its byte bound");
|
|
4819
|
-
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${
|
|
4820
|
-
hash.update(
|
|
4819
|
+
hash.update(`${Buffer.byteLength(file.path)}:${file.path}:${content2.byteLength}:`);
|
|
4820
|
+
hash.update(content2);
|
|
4821
4821
|
}
|
|
4822
4822
|
return `sha256:${hash.digest("hex")}`;
|
|
4823
4823
|
}
|
|
@@ -4912,13 +4912,13 @@ function createCodeRuntimeControlClient(options) {
|
|
|
4912
4912
|
if (!Number.isSafeInteger(modelRequestTimeoutMs) || modelRequestTimeoutMs < 3e4 || modelRequestTimeoutMs > 30 * 6e4) {
|
|
4913
4913
|
throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
|
|
4914
4914
|
}
|
|
4915
|
-
const
|
|
4915
|
+
const request2 = options.fetch ?? fetch;
|
|
4916
4916
|
const call2 = async (path, body, timeoutMs = requestTimeoutMs) => {
|
|
4917
4917
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
4918
4918
|
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
4919
4919
|
let response2;
|
|
4920
4920
|
try {
|
|
4921
|
-
response2 = await
|
|
4921
|
+
response2 = await request2(`${endpoint}${path}`, {
|
|
4922
4922
|
method: "POST",
|
|
4923
4923
|
headers: { authorization: `Bearer ${options.token}`, "content-type": "application/json" },
|
|
4924
4924
|
body: JSON.stringify(body),
|
|
@@ -5825,34 +5825,34 @@ function createCodeToolBroker(options) {
|
|
|
5825
5825
|
const policy = createCodePolicyGate(options);
|
|
5826
5826
|
let tail = Promise.resolve();
|
|
5827
5827
|
return {
|
|
5828
|
-
execute(context,
|
|
5829
|
-
const result = tail.then(() => route(context,
|
|
5828
|
+
execute(context, request2) {
|
|
5829
|
+
const result = tail.then(() => route(context, request2, options, recipes, policy));
|
|
5830
5830
|
tail = result.then(() => void 0, () => void 0);
|
|
5831
5831
|
return result;
|
|
5832
5832
|
}
|
|
5833
5833
|
};
|
|
5834
5834
|
}
|
|
5835
|
-
async function route(context,
|
|
5835
|
+
async function route(context, request2, options, recipes, policy) {
|
|
5836
5836
|
try {
|
|
5837
5837
|
if (context.signal?.aborted) throw new TypeError("tool request was cancelled");
|
|
5838
|
-
if (
|
|
5839
|
-
if (
|
|
5840
|
-
return await recipe(context,
|
|
5838
|
+
if (request2.tool === "sandbox.read") return await read(context, request2, options, policy);
|
|
5839
|
+
if (request2.tool === "sandbox.apply_patch") return await patch(context, request2, options, policy);
|
|
5840
|
+
return await recipe(context, request2, options, recipes, policy);
|
|
5841
5841
|
} catch (reason) {
|
|
5842
|
-
return response(
|
|
5842
|
+
return response(request2, false, reason instanceof TypeError ? reason.message : "tool failed closed");
|
|
5843
5843
|
}
|
|
5844
5844
|
}
|
|
5845
|
-
async function read(context,
|
|
5846
|
-
exactKeys(
|
|
5847
|
-
const path = stringField(
|
|
5848
|
-
const startLine = optionalInteger(
|
|
5849
|
-
const endLine = optionalInteger(
|
|
5845
|
+
async function read(context, request2, options, policy) {
|
|
5846
|
+
exactKeys(request2.input, ["path", "startLine", "endLine"]);
|
|
5847
|
+
const path = stringField(request2.input, "path");
|
|
5848
|
+
const startLine = optionalInteger(request2.input.startLine) ?? 1;
|
|
5849
|
+
const endLine = optionalInteger(request2.input.endLine) ?? startLine + (options.maxReadLines ?? 2e3) - 1;
|
|
5850
5850
|
if (endLine < startLine || endLine - startLine + 1 > (options.maxReadLines ?? 2e3)) {
|
|
5851
5851
|
throw new TypeError("requested line range exceeds its bound");
|
|
5852
5852
|
}
|
|
5853
5853
|
const paths = await registeredFiles(context.workspaceDir, 2e4);
|
|
5854
|
-
const allowed = await policy.read(policyContext(context,
|
|
5855
|
-
if (!allowed) return response(
|
|
5854
|
+
const allowed = await policy.read(policyContext(context, request2, options, { paths, path, startLine, endLine }));
|
|
5855
|
+
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
5856
5856
|
const target = resolveCodePath(context.workspaceDir, path);
|
|
5857
5857
|
const info = await (0, import_promises9.stat)(target);
|
|
5858
5858
|
if (!info.isFile() || info.size > Math.max(options.maxReadBytes ?? 128 * 1024, 2 * 1024 * 1024)) {
|
|
@@ -5861,40 +5861,40 @@ async function read(context, request, options, policy) {
|
|
|
5861
5861
|
const source = await (0, import_promises9.readFile)(target);
|
|
5862
5862
|
if (source.includes(0)) throw new TypeError("binary files are not readable through this tool");
|
|
5863
5863
|
const lines = source.toString("utf8").split("\n");
|
|
5864
|
-
const
|
|
5865
|
-
if (Buffer.byteLength(
|
|
5864
|
+
const content2 = lines.slice(startLine - 1, endLine).join("\n");
|
|
5865
|
+
if (Buffer.byteLength(content2) > (options.maxReadBytes ?? 128 * 1024)) {
|
|
5866
5866
|
throw new TypeError("read result exceeds its byte bound");
|
|
5867
5867
|
}
|
|
5868
|
-
return response(
|
|
5868
|
+
return response(request2, true, content2, { path, startLine, endLine: Math.min(endLine, lines.length) });
|
|
5869
5869
|
}
|
|
5870
|
-
async function patch(context,
|
|
5871
|
-
exactKeys(
|
|
5872
|
-
const value = stringField(
|
|
5870
|
+
async function patch(context, request2, options, policy) {
|
|
5871
|
+
exactKeys(request2.input, ["patch"]);
|
|
5872
|
+
const value = stringField(request2.input, "patch");
|
|
5873
5873
|
const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
|
|
5874
5874
|
if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
|
|
5875
5875
|
throw new TypeError("patch targets a read-only reference source");
|
|
5876
5876
|
}
|
|
5877
|
-
const allowed = await policy.patch(policyContext(context,
|
|
5878
|
-
if (!allowed) return response(
|
|
5877
|
+
const allowed = await policy.patch(policyContext(context, request2, options, { patch: value }));
|
|
5878
|
+
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
5879
5879
|
await applyCodePatch(context.workspaceDir, value, paths);
|
|
5880
|
-
return response(
|
|
5880
|
+
return response(request2, true, `Applied patch to ${paths.length} file(s).`, { paths });
|
|
5881
5881
|
}
|
|
5882
|
-
async function recipe(context,
|
|
5883
|
-
exactKeys(
|
|
5884
|
-
const recipeId = stringField(
|
|
5882
|
+
async function recipe(context, request2, options, recipes, policy) {
|
|
5883
|
+
exactKeys(request2.input, ["recipeId"]);
|
|
5884
|
+
const recipeId = stringField(request2.input, "recipeId");
|
|
5885
5885
|
const digestLimits = {
|
|
5886
5886
|
maxFiles: options.maxRecipeWorkspaceFiles ?? 2e4,
|
|
5887
5887
|
maxBytes: options.maxRecipeWorkspaceBytes ?? 512 * 1024 * 1024
|
|
5888
5888
|
};
|
|
5889
5889
|
const sourceDigest = await digestStagedWorkspace(context.workspaceDir, digestLimits);
|
|
5890
|
-
const allowed = await policy.recipe(policyContext(context,
|
|
5890
|
+
const allowed = await policy.recipe(policyContext(context, request2, options, {
|
|
5891
5891
|
recipeIds: [...recipes.keys()].sort(),
|
|
5892
5892
|
recipeId,
|
|
5893
5893
|
sourceDigest
|
|
5894
5894
|
}));
|
|
5895
|
-
if (!allowed) return response(
|
|
5895
|
+
if (!allowed) return response(request2, false, "tool denied by CaMeL policy");
|
|
5896
5896
|
const selected = recipes.get(recipeId);
|
|
5897
|
-
if (!selected) return response(
|
|
5897
|
+
if (!selected) return response(request2, false, "build recipe is not registered");
|
|
5898
5898
|
const staged = await stageWorkspace(context.workspaceDir, {
|
|
5899
5899
|
maxFiles: digestLimits.maxFiles,
|
|
5900
5900
|
maxBytes: digestLimits.maxBytes
|
|
@@ -5911,7 +5911,7 @@ async function recipe(context, request, options, recipes, policy) {
|
|
|
5911
5911
|
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
|
5912
5912
|
const ok = result.exitCode === 0 && !result.outputLimitExceeded && !result.timedOut;
|
|
5913
5913
|
const status = result.timedOut ? "timed out" : result.outputLimitExceeded ? "exceeded output limit" : ok ? "passed" : `failed with exit ${result.exitCode}`;
|
|
5914
|
-
return response(
|
|
5914
|
+
return response(request2, ok, `Recipe ${recipeId} ${status}.${output ? `
|
|
5915
5915
|
${output}` : ""}`, {
|
|
5916
5916
|
recipeId,
|
|
5917
5917
|
exitCode: result.exitCode,
|
|
@@ -5923,10 +5923,10 @@ ${output}` : ""}`, {
|
|
|
5923
5923
|
await staged.cleanup();
|
|
5924
5924
|
}
|
|
5925
5925
|
}
|
|
5926
|
-
function policyContext(context,
|
|
5926
|
+
function policyContext(context, request2, options, extra) {
|
|
5927
5927
|
return {
|
|
5928
5928
|
lease: context.lease,
|
|
5929
|
-
request,
|
|
5929
|
+
request: request2,
|
|
5930
5930
|
workspaceId: `workspace:${context.lease.task.attemptId}`,
|
|
5931
5931
|
readers: { kind: "principals", principalIds: [options.readerId] },
|
|
5932
5932
|
...extra
|
|
@@ -5976,8 +5976,8 @@ function optionalInteger(value) {
|
|
|
5976
5976
|
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("line bounds must be positive integers");
|
|
5977
5977
|
return value;
|
|
5978
5978
|
}
|
|
5979
|
-
function response(
|
|
5980
|
-
return { requestId:
|
|
5979
|
+
function response(request2, ok, content2, details) {
|
|
5980
|
+
return { requestId: request2.requestId, ok, content: content2, ...details ? { details } : {} };
|
|
5981
5981
|
}
|
|
5982
5982
|
function codeCommandMetadata(payload, resume) {
|
|
5983
5983
|
const trusted = record22(payload.trustedBase);
|
|
@@ -6077,13 +6077,13 @@ function createCodeRuntimeToolBroker(input, lease, role) {
|
|
|
6077
6077
|
readerId: `code-session:${lease.task.taskId}`,
|
|
6078
6078
|
readOnlyPrefixes: [".odla-references"]
|
|
6079
6079
|
});
|
|
6080
|
-
return role === "coding" ? broker : { execute: (context,
|
|
6080
|
+
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" }) };
|
|
6081
6081
|
}
|
|
6082
6082
|
async function handleCodeRuntimeInference(input) {
|
|
6083
|
-
const { command, metadata: metadata2, request, state } = input;
|
|
6084
|
-
if (
|
|
6085
|
-
if (!
|
|
6086
|
-
|
|
6083
|
+
const { command, metadata: metadata2, request: request2, state: state2 } = input;
|
|
6084
|
+
if (state2.tokens >= metadata2.maxTokensPerInteraction) {
|
|
6085
|
+
if (!state2.noticeEmitted) {
|
|
6086
|
+
state2.noticeEmitted = true;
|
|
6087
6087
|
await input.event({
|
|
6088
6088
|
type: "message",
|
|
6089
6089
|
actor: "system",
|
|
@@ -6093,7 +6093,7 @@ async function handleCodeRuntimeInference(input) {
|
|
|
6093
6093
|
return {
|
|
6094
6094
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
6095
6095
|
type: "inference.response",
|
|
6096
|
-
requestId:
|
|
6096
|
+
requestId: request2.requestId,
|
|
6097
6097
|
response: {
|
|
6098
6098
|
id: `budget:${command.commandId}`,
|
|
6099
6099
|
provider: "openai",
|
|
@@ -6107,11 +6107,11 @@ async function handleCodeRuntimeInference(input) {
|
|
|
6107
6107
|
}
|
|
6108
6108
|
const startedAt = Date.now();
|
|
6109
6109
|
const response2 = await input.control.infer(command.sessionId, {
|
|
6110
|
-
requestId:
|
|
6110
|
+
requestId: request2.requestId,
|
|
6111
6111
|
interactionId: command.commandId,
|
|
6112
|
-
call:
|
|
6112
|
+
call: request2.call
|
|
6113
6113
|
});
|
|
6114
|
-
|
|
6114
|
+
state2.tokens += response2.receipt.inputTokens + response2.receipt.outputTokens;
|
|
6115
6115
|
await input.event({
|
|
6116
6116
|
type: "usage",
|
|
6117
6117
|
provider: response2.receipt.provider,
|
|
@@ -6120,13 +6120,13 @@ async function handleCodeRuntimeInference(input) {
|
|
|
6120
6120
|
outputTokens: response2.receipt.outputTokens,
|
|
6121
6121
|
durationMs: Date.now() - startedAt,
|
|
6122
6122
|
interactionId: command.commandId,
|
|
6123
|
-
interactionTokens:
|
|
6123
|
+
interactionTokens: state2.tokens,
|
|
6124
6124
|
interactionMaxTokens: metadata2.maxTokensPerInteraction
|
|
6125
6125
|
}).catch(() => void 0);
|
|
6126
6126
|
return {
|
|
6127
6127
|
protocolVersion: HARNESS_PROTOCOL_VERSION,
|
|
6128
6128
|
type: "inference.response",
|
|
6129
|
-
requestId:
|
|
6129
|
+
requestId: request2.requestId,
|
|
6130
6130
|
response: response2.response
|
|
6131
6131
|
};
|
|
6132
6132
|
}
|
|
@@ -6494,6 +6494,14 @@ Usage:
|
|
|
6494
6494
|
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
6495
6495
|
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
6496
6496
|
odla-ai pm handoff --app <id> [--json]
|
|
6497
|
+
odla-ai discuss groups [--json]
|
|
6498
|
+
odla-ai discuss list [--app <id>] [--q <text>] [--state open|resolved|all] [--json]
|
|
6499
|
+
odla-ai discuss read <topic> [--json]
|
|
6500
|
+
odla-ai discuss post --app <id> --subject "..." --body "..." [--markup "... @[Label](kind/id)"]
|
|
6501
|
+
odla-ai discuss reply <topic> --body "..." [--markup "..."]
|
|
6502
|
+
odla-ai discuss resolve <topic> [--reopen]
|
|
6503
|
+
odla-ai discuss who --q <text> [--app <id>] [--kinds user,pm:task] [--json]
|
|
6504
|
+
odla-ai discuss watch [<topic>] [--by <authorId>] [--self <authorId>] [--interval <s>] [--timeout <s>] [--json]
|
|
6497
6505
|
odla-ai whoami [--json]
|
|
6498
6506
|
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
6499
6507
|
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
@@ -6598,6 +6606,13 @@ Commands:
|
|
|
6598
6606
|
"app". Entities: goal (alias conformance), task (alias kanban),
|
|
6599
6607
|
decision, bug. Status changes and comments post to each item's
|
|
6600
6608
|
@odla-ai/chat discussion thread.
|
|
6609
|
+
discuss Group discussions (via @odla-ai/chat) for the apps you co-own:
|
|
6610
|
+
one group per project, topics with replies, @-mentions of people,
|
|
6611
|
+
agents, PM items, and projects. Built for unattended use \u2014 post a
|
|
6612
|
+
question, then "watch" it until someone answers. "watch" exits 75
|
|
6613
|
+
(not 1) when it times out with nothing new, so a script can tell
|
|
6614
|
+
"no answer yet" from a failure and just rerun. Use "who" to look
|
|
6615
|
+
up the exact @[Label](kind/id) markup for a mention.
|
|
6601
6616
|
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
6602
6617
|
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
6603
6618
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
@@ -6785,15 +6800,15 @@ async function connectGitHubSecuritySource(options) {
|
|
|
6785
6800
|
const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
|
|
6786
6801
|
const wait2 = options.wait ?? waitForHostedPoll;
|
|
6787
6802
|
while (true) {
|
|
6788
|
-
const
|
|
6803
|
+
const state2 = await requestHostedSecurityJson(
|
|
6789
6804
|
options,
|
|
6790
6805
|
`/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
|
|
6791
6806
|
{},
|
|
6792
6807
|
"check GitHub connection"
|
|
6793
6808
|
);
|
|
6794
|
-
if (
|
|
6795
|
-
if (
|
|
6796
|
-
throw new Error(`GitHub connection ${
|
|
6809
|
+
if (state2.status !== "pending") {
|
|
6810
|
+
if (state2.status === "connected") return state2;
|
|
6811
|
+
throw new Error(`GitHub connection ${state2.status}${state2.failureCode ? `: ${state2.failureCode}` : ""}`);
|
|
6797
6812
|
}
|
|
6798
6813
|
if (now() >= deadline) throw new Error("GitHub connection approval timed out");
|
|
6799
6814
|
await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
@@ -7244,6 +7259,289 @@ async function codeCommand(parsed, dependencies) {
|
|
|
7244
7259
|
});
|
|
7245
7260
|
}
|
|
7246
7261
|
|
|
7262
|
+
// src/discuss-actions.ts
|
|
7263
|
+
async function request(ctx, method, path, body) {
|
|
7264
|
+
const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
|
|
7265
|
+
method,
|
|
7266
|
+
headers: { authorization: `Bearer ${ctx.token}`, "content-type": "application/json" },
|
|
7267
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
7268
|
+
});
|
|
7269
|
+
const data = await res.json().catch(() => ({}));
|
|
7270
|
+
if (!res.ok)
|
|
7271
|
+
throw new Error(`discuss ${method} ${path} failed: ${data.error ?? `registry returned ${res.status}`}`);
|
|
7272
|
+
return data;
|
|
7273
|
+
}
|
|
7274
|
+
function emit(ctx, value, human) {
|
|
7275
|
+
if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
|
|
7276
|
+
else human();
|
|
7277
|
+
}
|
|
7278
|
+
var state = (topic) => topic.resolvedAt ? "resolved" : "open";
|
|
7279
|
+
function bodyWithRefs(post) {
|
|
7280
|
+
if (!post.refs || post.refs.length === 0) return post.body;
|
|
7281
|
+
let out = "";
|
|
7282
|
+
let cursor = 0;
|
|
7283
|
+
for (const ref of [...post.refs].sort((a, b) => a.start - b.start)) {
|
|
7284
|
+
if (ref.start < cursor || ref.end > post.body.length) continue;
|
|
7285
|
+
out += post.body.slice(cursor, ref.start) + `@[${ref.label}](${ref.kind}/${ref.id})`;
|
|
7286
|
+
cursor = ref.end;
|
|
7287
|
+
}
|
|
7288
|
+
return out + post.body.slice(cursor);
|
|
7289
|
+
}
|
|
7290
|
+
function content(parsed) {
|
|
7291
|
+
const markup = stringOpt(parsed.options.markup);
|
|
7292
|
+
if (markup) return { markup };
|
|
7293
|
+
const body = stringOpt(parsed.options.body);
|
|
7294
|
+
if (!body) throw new Error('needs --body "\u2026" (or --markup with @[Label](kind/id) mentions)');
|
|
7295
|
+
return { body };
|
|
7296
|
+
}
|
|
7297
|
+
async function discussGroups(ctx) {
|
|
7298
|
+
const result = await request(
|
|
7299
|
+
ctx,
|
|
7300
|
+
"GET",
|
|
7301
|
+
"/groups"
|
|
7302
|
+
);
|
|
7303
|
+
emit(ctx, result, () => {
|
|
7304
|
+
ctx.out.log("app name started");
|
|
7305
|
+
for (const group of result.groups) {
|
|
7306
|
+
ctx.out.log(`${group.appId} ${group.name} ${group.channelId ? "yes" : "no"}`);
|
|
7307
|
+
}
|
|
7308
|
+
});
|
|
7309
|
+
}
|
|
7310
|
+
async function discussList(ctx, parsed) {
|
|
7311
|
+
const query = new URLSearchParams();
|
|
7312
|
+
for (const [flag, param] of [
|
|
7313
|
+
["app", "app"],
|
|
7314
|
+
["q", "q"],
|
|
7315
|
+
["state", "state"],
|
|
7316
|
+
["limit", "limit"],
|
|
7317
|
+
["offset", "offset"]
|
|
7318
|
+
]) {
|
|
7319
|
+
const value = stringOpt(parsed.options[flag]);
|
|
7320
|
+
if (value) query.set(param, value);
|
|
7321
|
+
}
|
|
7322
|
+
const qs = query.toString();
|
|
7323
|
+
const page = await request(ctx, "GET", `/topics${qs ? `?${qs}` : ""}`);
|
|
7324
|
+
emit(ctx, page, () => {
|
|
7325
|
+
ctx.out.log(`topics \u2014 ${page.topics.length} of ${page.total}`);
|
|
7326
|
+
ctx.out.log("id state app replies subject");
|
|
7327
|
+
for (const topic of page.topics) {
|
|
7328
|
+
ctx.out.log(
|
|
7329
|
+
`${topic.id} ${state(topic)} ${topic.appId ?? ""} ${topic.replyCount} ${topic.subject}`
|
|
7330
|
+
);
|
|
7331
|
+
}
|
|
7332
|
+
});
|
|
7333
|
+
}
|
|
7334
|
+
async function discussRead(ctx, id) {
|
|
7335
|
+
const result = await request(
|
|
7336
|
+
ctx,
|
|
7337
|
+
"GET",
|
|
7338
|
+
`/topics/${encodeURIComponent(id)}`
|
|
7339
|
+
);
|
|
7340
|
+
emit(ctx, result, () => {
|
|
7341
|
+
ctx.out.log(`${result.topic.subject} [${state(result.topic)}] ${result.topic.appId ?? ""}`);
|
|
7342
|
+
for (const post of result.posts) {
|
|
7343
|
+
const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
|
|
7344
|
+
ctx.out.log(`
|
|
7345
|
+
\u2014 ${who}`);
|
|
7346
|
+
ctx.out.log(bodyWithRefs(post));
|
|
7347
|
+
for (const file of post.attachments ?? []) ctx.out.log(` [attachment] ${file.name} (${file.size} bytes)`);
|
|
7348
|
+
}
|
|
7349
|
+
});
|
|
7350
|
+
}
|
|
7351
|
+
async function discussPost(ctx, parsed) {
|
|
7352
|
+
const appId = stringOpt(parsed.options.app);
|
|
7353
|
+
if (!appId) throw new Error("discuss post needs --app <appId>");
|
|
7354
|
+
const subject = stringOpt(parsed.options.subject);
|
|
7355
|
+
if (!subject) throw new Error('discuss post needs --subject "\u2026"');
|
|
7356
|
+
const created = await request(ctx, "POST", "/topics", {
|
|
7357
|
+
appId,
|
|
7358
|
+
subject,
|
|
7359
|
+
...content(parsed)
|
|
7360
|
+
});
|
|
7361
|
+
emit(ctx, created, () => ctx.out.log(`opened topic ${created.id}`));
|
|
7362
|
+
}
|
|
7363
|
+
async function discussReply(ctx, id, parsed) {
|
|
7364
|
+
const created = await request(
|
|
7365
|
+
ctx,
|
|
7366
|
+
"POST",
|
|
7367
|
+
`/topics/${encodeURIComponent(id)}/replies`,
|
|
7368
|
+
content(parsed)
|
|
7369
|
+
);
|
|
7370
|
+
emit(ctx, created, () => ctx.out.log(`replied ${created.id}`));
|
|
7371
|
+
}
|
|
7372
|
+
async function discussResolve(ctx, id, resolved) {
|
|
7373
|
+
const result = await request(
|
|
7374
|
+
ctx,
|
|
7375
|
+
"PATCH",
|
|
7376
|
+
`/topics/${encodeURIComponent(id)}`,
|
|
7377
|
+
{ resolved }
|
|
7378
|
+
);
|
|
7379
|
+
emit(ctx, result, () => ctx.out.log(`${resolved ? "resolved" : "reopened"} ${result.id}`));
|
|
7380
|
+
}
|
|
7381
|
+
async function discussWho(ctx, parsed) {
|
|
7382
|
+
const query = new URLSearchParams({ q: stringOpt(parsed.options.q) ?? "" });
|
|
7383
|
+
const app = stringOpt(parsed.options.app);
|
|
7384
|
+
if (app) query.set("app", app);
|
|
7385
|
+
const kinds = stringOpt(parsed.options.kinds);
|
|
7386
|
+
if (kinds) query.set("kinds", kinds);
|
|
7387
|
+
const found = await request(ctx, "GET", `/mentionables?${query.toString()}`);
|
|
7388
|
+
emit(ctx, found, () => {
|
|
7389
|
+
ctx.out.log("mention kind hint");
|
|
7390
|
+
for (const item of found.items) {
|
|
7391
|
+
ctx.out.log(`@[${item.label}](${item.kind}/${item.id}) ${item.kind} ${item.hint ?? ""}`);
|
|
7392
|
+
}
|
|
7393
|
+
if (found.failed.length > 0) ctx.out.error(`(no results from: ${found.failed.join(", ")})`);
|
|
7394
|
+
});
|
|
7395
|
+
}
|
|
7396
|
+
|
|
7397
|
+
// src/discuss-watch.ts
|
|
7398
|
+
var DEFAULT_INTERVAL_MS = 15e3;
|
|
7399
|
+
var DEFAULT_TIMEOUT_MS = 15 * 6e4;
|
|
7400
|
+
function numberOpt2(parsed, flag, fallback) {
|
|
7401
|
+
const raw = stringOpt(parsed.options[flag]);
|
|
7402
|
+
const value = raw == null ? NaN : Number(raw);
|
|
7403
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
7404
|
+
}
|
|
7405
|
+
async function get(ctx, path) {
|
|
7406
|
+
const res = await ctx.doFetch(`${ctx.platformUrl}/registry/discuss${path}`, {
|
|
7407
|
+
headers: { authorization: `Bearer ${ctx.token}` }
|
|
7408
|
+
});
|
|
7409
|
+
const data = await res.json().catch(() => ({}));
|
|
7410
|
+
if (!res.ok) throw new Error(`discuss watch failed: ${data.error ?? `registry returned ${res.status}`}`);
|
|
7411
|
+
return data;
|
|
7412
|
+
}
|
|
7413
|
+
async function discussWatch(ctx, topicId, parsed) {
|
|
7414
|
+
const sleep = ctx.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
|
|
7415
|
+
const now = ctx.now ?? Date.now;
|
|
7416
|
+
const intervalMs = numberOpt2(parsed, "interval", DEFAULT_INTERVAL_MS / 1e3) * 1e3;
|
|
7417
|
+
const timeoutMs = numberOpt2(parsed, "timeout", DEFAULT_TIMEOUT_MS / 1e3) * 1e3;
|
|
7418
|
+
const by = stringOpt(parsed.options.by);
|
|
7419
|
+
const self = stringOpt(parsed.options.self);
|
|
7420
|
+
const deadline = now() + timeoutMs;
|
|
7421
|
+
const fresh = (post) => (!by || post.authorId === by) && (!self || post.authorId !== self);
|
|
7422
|
+
let seen = /* @__PURE__ */ new Set();
|
|
7423
|
+
let since = now();
|
|
7424
|
+
if (topicId) {
|
|
7425
|
+
const first = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
|
|
7426
|
+
seen = new Set(first.posts.map((post) => post.id));
|
|
7427
|
+
}
|
|
7428
|
+
for (; ; ) {
|
|
7429
|
+
if (topicId) {
|
|
7430
|
+
const { posts } = await get(ctx, `/topics/${encodeURIComponent(topicId)}`);
|
|
7431
|
+
const added = posts.filter((post) => !seen.has(post.id) && fresh(post));
|
|
7432
|
+
if (added.length > 0) return report2(ctx, { found: true, posts: added });
|
|
7433
|
+
for (const post of posts) seen.add(post.id);
|
|
7434
|
+
} else {
|
|
7435
|
+
const app = stringOpt(parsed.options.app);
|
|
7436
|
+
const { topics } = await get(
|
|
7437
|
+
ctx,
|
|
7438
|
+
`/topics?state=all${app ? `&app=${encodeURIComponent(app)}` : ""}`
|
|
7439
|
+
);
|
|
7440
|
+
const active = topics.filter((topic) => topic.lastActivityAt > since);
|
|
7441
|
+
if (active.length > 0) return report2(ctx, { found: true, topics: active });
|
|
7442
|
+
since = now();
|
|
7443
|
+
}
|
|
7444
|
+
if (now() >= deadline) return report2(ctx, { found: false });
|
|
7445
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
|
|
7446
|
+
}
|
|
7447
|
+
}
|
|
7448
|
+
function report2(ctx, result) {
|
|
7449
|
+
if (ctx.json) {
|
|
7450
|
+
ctx.out.log(JSON.stringify(result, null, 2));
|
|
7451
|
+
} else if (result.found) {
|
|
7452
|
+
for (const post of result.posts ?? []) {
|
|
7453
|
+
const who = post.authorKind === "bot" ? `${post.authorId} (agent)` : post.authorId;
|
|
7454
|
+
ctx.out.log(`\u2014 ${who}
|
|
7455
|
+
${post.body}`);
|
|
7456
|
+
}
|
|
7457
|
+
for (const topic of result.topics ?? []) {
|
|
7458
|
+
ctx.out.log(`${topic.id} ${topic.appId ?? ""} ${topic.subject}`);
|
|
7459
|
+
}
|
|
7460
|
+
}
|
|
7461
|
+
return result;
|
|
7462
|
+
}
|
|
7463
|
+
var WatchTimeoutError = class extends Error {
|
|
7464
|
+
code = "watch_timeout";
|
|
7465
|
+
constructor() {
|
|
7466
|
+
super("nothing new before the timeout");
|
|
7467
|
+
this.name = "WatchTimeoutError";
|
|
7468
|
+
}
|
|
7469
|
+
};
|
|
7470
|
+
|
|
7471
|
+
// src/discuss-command.ts
|
|
7472
|
+
var ALLOWED = [
|
|
7473
|
+
"config",
|
|
7474
|
+
"token",
|
|
7475
|
+
"email",
|
|
7476
|
+
"json",
|
|
7477
|
+
"app",
|
|
7478
|
+
"subject",
|
|
7479
|
+
"body",
|
|
7480
|
+
"markup",
|
|
7481
|
+
"state",
|
|
7482
|
+
"q",
|
|
7483
|
+
"kinds",
|
|
7484
|
+
"limit",
|
|
7485
|
+
"offset",
|
|
7486
|
+
"reopen",
|
|
7487
|
+
"by",
|
|
7488
|
+
"self",
|
|
7489
|
+
"interval",
|
|
7490
|
+
"timeout"
|
|
7491
|
+
];
|
|
7492
|
+
function requireId(id, action) {
|
|
7493
|
+
if (!id) throw new Error(`"discuss ${action}" needs a topic id`);
|
|
7494
|
+
return id;
|
|
7495
|
+
}
|
|
7496
|
+
async function buildContext(parsed, deps) {
|
|
7497
|
+
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
7498
|
+
const doFetch = deps.fetch ?? fetch;
|
|
7499
|
+
const out = deps.stdout ?? console;
|
|
7500
|
+
const token = await getDeveloperToken(
|
|
7501
|
+
cfg,
|
|
7502
|
+
{
|
|
7503
|
+
configPath: cfg.configPath,
|
|
7504
|
+
token: stringOpt(parsed.options.token),
|
|
7505
|
+
email: stringOpt(parsed.options.email),
|
|
7506
|
+
open: false
|
|
7507
|
+
},
|
|
7508
|
+
doFetch,
|
|
7509
|
+
out
|
|
7510
|
+
);
|
|
7511
|
+
return { platformUrl: cfg.platformUrl, token, doFetch, out, json: parsed.options.json === true };
|
|
7512
|
+
}
|
|
7513
|
+
async function discussCommand(parsed, deps = {}) {
|
|
7514
|
+
assertArgs(parsed, ALLOWED, 3);
|
|
7515
|
+
const action = parsed.positionals[1];
|
|
7516
|
+
const id = parsed.positionals[2];
|
|
7517
|
+
if (!action) throw new Error('"discuss" needs an action. Run "odla-ai help".');
|
|
7518
|
+
const ctx = await buildContext(parsed, deps);
|
|
7519
|
+
switch (action) {
|
|
7520
|
+
case "groups":
|
|
7521
|
+
return discussGroups(ctx);
|
|
7522
|
+
case "list":
|
|
7523
|
+
case "topics":
|
|
7524
|
+
return discussList(ctx, parsed);
|
|
7525
|
+
case "read":
|
|
7526
|
+
return discussRead(ctx, requireId(id, "read"));
|
|
7527
|
+
case "post":
|
|
7528
|
+
return discussPost(ctx, parsed);
|
|
7529
|
+
case "reply":
|
|
7530
|
+
return discussReply(ctx, requireId(id, "reply"), parsed);
|
|
7531
|
+
case "resolve":
|
|
7532
|
+
return discussResolve(ctx, requireId(id, "resolve"), parsed.options.reopen !== true);
|
|
7533
|
+
case "who":
|
|
7534
|
+
return discussWho(ctx, parsed);
|
|
7535
|
+
case "watch": {
|
|
7536
|
+
const result = await discussWatch(ctx, id, parsed);
|
|
7537
|
+
if (!result.found) throw new WatchTimeoutError();
|
|
7538
|
+
return;
|
|
7539
|
+
}
|
|
7540
|
+
default:
|
|
7541
|
+
throw new Error(`unknown discuss action "${action}". Run "odla-ai help".`);
|
|
7542
|
+
}
|
|
7543
|
+
}
|
|
7544
|
+
|
|
7247
7545
|
// src/pm-actions.ts
|
|
7248
7546
|
var FIELD_MAP = {
|
|
7249
7547
|
title: { key: "title" },
|
|
@@ -7307,7 +7605,7 @@ function statusCol(entity, r) {
|
|
|
7307
7605
|
function printRecord(ctx, entity, r) {
|
|
7308
7606
|
ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
|
|
7309
7607
|
}
|
|
7310
|
-
function
|
|
7608
|
+
function emit2(ctx, value, human) {
|
|
7311
7609
|
if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
|
|
7312
7610
|
else human();
|
|
7313
7611
|
}
|
|
@@ -7333,7 +7631,7 @@ async function pmList(ctx, entity, parsed) {
|
|
|
7333
7631
|
}
|
|
7334
7632
|
const qs = q.toString();
|
|
7335
7633
|
const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
|
|
7336
|
-
|
|
7634
|
+
emit2(ctx, page, () => {
|
|
7337
7635
|
ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
|
|
7338
7636
|
ctx.out.log("id state app title");
|
|
7339
7637
|
for (const r of page.records) printRecord(ctx, entity, r);
|
|
@@ -7347,25 +7645,25 @@ async function pmAdd(ctx, entity, parsed) {
|
|
|
7347
7645
|
if (entity === "bug" && !input.description)
|
|
7348
7646
|
throw new Error("pm bug add needs --description <context> (or --body <context>)");
|
|
7349
7647
|
const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
|
|
7350
|
-
|
|
7648
|
+
emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
|
|
7351
7649
|
}
|
|
7352
7650
|
async function pmGet(ctx, entity, id) {
|
|
7353
7651
|
const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
|
|
7354
|
-
|
|
7652
|
+
emit2(ctx, record5, () => printRecord(ctx, entity, record5));
|
|
7355
7653
|
}
|
|
7356
7654
|
async function pmSet(ctx, entity, id, parsed) {
|
|
7357
7655
|
const patch2 = collectEntityFields(entity, parsed, true);
|
|
7358
7656
|
if (Object.keys(patch2).length === 0)
|
|
7359
7657
|
throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
|
|
7360
7658
|
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
7361
|
-
|
|
7659
|
+
emit2(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
|
|
7362
7660
|
}
|
|
7363
7661
|
async function pmDone(ctx, entity, id, parsed) {
|
|
7364
7662
|
const decisionId = stringOpt(parsed.options.decision);
|
|
7365
7663
|
if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
|
|
7366
7664
|
const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
|
|
7367
7665
|
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
7368
|
-
|
|
7666
|
+
emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
|
|
7369
7667
|
}
|
|
7370
7668
|
async function allRecords(ctx, entity, appId) {
|
|
7371
7669
|
const records = [];
|
|
@@ -7398,7 +7696,7 @@ async function pmHandoff(ctx, parsed) {
|
|
|
7398
7696
|
...handoff,
|
|
7399
7697
|
clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
|
|
7400
7698
|
};
|
|
7401
|
-
|
|
7699
|
+
emit2(ctx, result, () => {
|
|
7402
7700
|
if (result.clean) {
|
|
7403
7701
|
ctx.out.log(`${appId}: no unresolved PM work`);
|
|
7404
7702
|
return;
|
|
@@ -7431,7 +7729,7 @@ async function pmComments(ctx, entity, id) {
|
|
|
7431
7729
|
"GET",
|
|
7432
7730
|
`/${entity}/${encodeURIComponent(id)}/comments`
|
|
7433
7731
|
);
|
|
7434
|
-
|
|
7732
|
+
emit2(ctx, messages, () => {
|
|
7435
7733
|
if (messages.length === 0) ctx.out.log("(no comments)");
|
|
7436
7734
|
else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
|
|
7437
7735
|
});
|
|
@@ -7450,7 +7748,7 @@ var ALIASES = {
|
|
|
7450
7748
|
decision: "decision",
|
|
7451
7749
|
bug: "bug"
|
|
7452
7750
|
};
|
|
7453
|
-
var
|
|
7751
|
+
var ALLOWED2 = [
|
|
7454
7752
|
"config",
|
|
7455
7753
|
"token",
|
|
7456
7754
|
"email",
|
|
@@ -7474,11 +7772,11 @@ var ALLOWED = [
|
|
|
7474
7772
|
"offset",
|
|
7475
7773
|
"q"
|
|
7476
7774
|
];
|
|
7477
|
-
function
|
|
7775
|
+
function requireId2(id, action) {
|
|
7478
7776
|
if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
|
|
7479
7777
|
return id;
|
|
7480
7778
|
}
|
|
7481
|
-
async function
|
|
7779
|
+
async function buildContext2(parsed, deps) {
|
|
7482
7780
|
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
7483
7781
|
const doFetch = deps.fetch ?? fetch;
|
|
7484
7782
|
const out = deps.stdout ?? console;
|
|
@@ -7493,14 +7791,14 @@ async function buildContext(parsed, deps) {
|
|
|
7493
7791
|
async function pmCommand(parsed, deps = {}) {
|
|
7494
7792
|
const word = parsed.positionals[1] ?? "";
|
|
7495
7793
|
if (word === "handoff") {
|
|
7496
|
-
assertArgs(parsed,
|
|
7497
|
-
return pmHandoff(await
|
|
7794
|
+
assertArgs(parsed, ALLOWED2, 2);
|
|
7795
|
+
return pmHandoff(await buildContext2(parsed, deps), parsed);
|
|
7498
7796
|
}
|
|
7499
7797
|
const entity = ALIASES[word];
|
|
7500
7798
|
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
7501
7799
|
const action = parsed.positionals[2] ?? "list";
|
|
7502
|
-
assertArgs(parsed,
|
|
7503
|
-
const ctx = await
|
|
7800
|
+
assertArgs(parsed, ALLOWED2, 4);
|
|
7801
|
+
const ctx = await buildContext2(parsed, deps);
|
|
7504
7802
|
const id = parsed.positionals[3];
|
|
7505
7803
|
switch (action) {
|
|
7506
7804
|
case "list":
|
|
@@ -7509,21 +7807,21 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
7509
7807
|
case "create":
|
|
7510
7808
|
return pmAdd(ctx, entity, parsed);
|
|
7511
7809
|
case "get":
|
|
7512
|
-
return pmGet(ctx, entity,
|
|
7810
|
+
return pmGet(ctx, entity, requireId2(id, action));
|
|
7513
7811
|
case "set":
|
|
7514
7812
|
case "update":
|
|
7515
7813
|
case "status":
|
|
7516
7814
|
case "move":
|
|
7517
|
-
return pmSet(ctx, entity,
|
|
7815
|
+
return pmSet(ctx, entity, requireId2(id, action), parsed);
|
|
7518
7816
|
case "done":
|
|
7519
|
-
return pmDone(ctx, entity,
|
|
7817
|
+
return pmDone(ctx, entity, requireId2(id, action), parsed);
|
|
7520
7818
|
case "comment":
|
|
7521
|
-
return pmComment(ctx, entity,
|
|
7819
|
+
return pmComment(ctx, entity, requireId2(id, action), parsed);
|
|
7522
7820
|
case "comments":
|
|
7523
|
-
return pmComments(ctx, entity,
|
|
7821
|
+
return pmComments(ctx, entity, requireId2(id, action));
|
|
7524
7822
|
case "rm":
|
|
7525
7823
|
case "delete":
|
|
7526
|
-
return pmRemove(ctx, entity,
|
|
7824
|
+
return pmRemove(ctx, entity, requireId2(id, action));
|
|
7527
7825
|
default:
|
|
7528
7826
|
throw new Error(`unknown pm action "${action}". Try list|add|get|set|done|comment|comments|rm.`);
|
|
7529
7827
|
}
|
|
@@ -7989,6 +8287,18 @@ var COMMAND_SURFACE = {
|
|
|
7989
8287
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
7990
8288
|
capabilities: {},
|
|
7991
8289
|
code: { connect: {} },
|
|
8290
|
+
// `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
|
|
8291
|
+
discuss: {
|
|
8292
|
+
groups: {},
|
|
8293
|
+
list: {},
|
|
8294
|
+
topics: {},
|
|
8295
|
+
read: {},
|
|
8296
|
+
post: {},
|
|
8297
|
+
reply: {},
|
|
8298
|
+
resolve: {},
|
|
8299
|
+
who: {},
|
|
8300
|
+
watch: {}
|
|
8301
|
+
},
|
|
7992
8302
|
doctor: {},
|
|
7993
8303
|
help: {},
|
|
7994
8304
|
init: {},
|
|
@@ -8585,7 +8895,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
8585
8895
|
return out;
|
|
8586
8896
|
}
|
|
8587
8897
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
8588
|
-
function
|
|
8898
|
+
function report3(ctx, impacts) {
|
|
8589
8899
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
8590
8900
|
ctx.out.log(
|
|
8591
8901
|
`${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.`
|
|
@@ -8623,7 +8933,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
8623
8933
|
}
|
|
8624
8934
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
8625
8935
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
8626
|
-
|
|
8936
|
+
report3(ctx, impacts);
|
|
8627
8937
|
}
|
|
8628
8938
|
|
|
8629
8939
|
// src/runbook-lint.ts
|
|
@@ -8856,7 +9166,7 @@ async function whoamiCommand(parsed, deps = {}) {
|
|
|
8856
9166
|
}
|
|
8857
9167
|
|
|
8858
9168
|
// src/runbook-command.ts
|
|
8859
|
-
var
|
|
9169
|
+
var ALLOWED3 = [
|
|
8860
9170
|
"config",
|
|
8861
9171
|
"token",
|
|
8862
9172
|
"email",
|
|
@@ -8896,7 +9206,7 @@ async function loadOrDefaultConfig(configPath, action, appId) {
|
|
|
8896
9206
|
}
|
|
8897
9207
|
return loadProjectConfig(configPath);
|
|
8898
9208
|
}
|
|
8899
|
-
async function
|
|
9209
|
+
async function buildContext3(parsed, deps, action) {
|
|
8900
9210
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
8901
9211
|
const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
|
|
8902
9212
|
const doFetch = deps.fetch ?? fetch;
|
|
@@ -8952,8 +9262,8 @@ async function buildContext2(parsed, deps, action) {
|
|
|
8952
9262
|
}
|
|
8953
9263
|
async function runbookCommand(parsed, deps = {}) {
|
|
8954
9264
|
const action = parsed.positionals[1] ?? "list";
|
|
8955
|
-
assertArgs(parsed,
|
|
8956
|
-
const ctx = await
|
|
9265
|
+
assertArgs(parsed, ALLOWED3, action === "ask" || action === "search" ? 64 : 4);
|
|
9266
|
+
const ctx = await buildContext3(parsed, deps, action);
|
|
8957
9267
|
const slug = parsed.positionals[2];
|
|
8958
9268
|
switch (action) {
|
|
8959
9269
|
case "list":
|
|
@@ -9144,31 +9454,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
9144
9454
|
url.searchParams.set("job", job.jobId);
|
|
9145
9455
|
out.log(` Studio: ${url.toString()}`);
|
|
9146
9456
|
}
|
|
9147
|
-
function printHostedReport(out,
|
|
9148
|
-
out.log(`security report ${
|
|
9149
|
-
out.log(` coverage: ${
|
|
9150
|
-
out.log(` findings: confirmed=${
|
|
9151
|
-
out.log(` discovery: ${
|
|
9152
|
-
out.log(` validation: ${
|
|
9153
|
-
for (const finding of
|
|
9457
|
+
function printHostedReport(out, report4) {
|
|
9458
|
+
out.log(`security report ${report4.jobId}: ${report4.repository}@${report4.revision}`);
|
|
9459
|
+
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}`);
|
|
9460
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates} rejected=${report4.metrics.rejected}`);
|
|
9461
|
+
out.log(` discovery: ${report4.provenance.discovery?.provider ?? "unknown"}/${report4.provenance.discovery?.model ?? "unknown"}`);
|
|
9462
|
+
out.log(` validation: ${report4.provenance.validation?.provider ?? "unknown"}/${report4.provenance.validation?.model ?? "unknown"} independent=${String(report4.provenance.independentValidation)}`);
|
|
9463
|
+
for (const finding of report4.findings) {
|
|
9154
9464
|
const location = finding.locations[0];
|
|
9155
9465
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
9156
9466
|
}
|
|
9157
|
-
for (const limitation of
|
|
9467
|
+
for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
|
|
9158
9468
|
}
|
|
9159
|
-
function enforceHostedReportGate(
|
|
9469
|
+
function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
|
|
9160
9470
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
9161
9471
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
9162
9472
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
9163
9473
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
9164
|
-
const confirmed =
|
|
9165
|
-
const leads = failOnCandidates ?
|
|
9166
|
-
const incomplete =
|
|
9474
|
+
const confirmed = report4.findings.filter((finding) => finding.disposition === "confirmed" && atOrAbove(finding.severity, failOn));
|
|
9475
|
+
const leads = failOnCandidates ? report4.findings.filter((finding) => finding.disposition !== "confirmed" && atOrAbove(finding.severity, failOnCandidates)) : [];
|
|
9476
|
+
const incomplete = report4.coverageStatus !== "complete" && parsed.options["allow-incomplete"] !== true;
|
|
9167
9477
|
if (confirmed.length || leads.length || incomplete) {
|
|
9168
|
-
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${
|
|
9478
|
+
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? `; coverage ${report4.coverageStatus}` : ""}`);
|
|
9169
9479
|
}
|
|
9170
9480
|
if (emitSuccess) {
|
|
9171
|
-
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${
|
|
9481
|
+
out.log(`security gate passed: 0 confirmed >= ${failOn}; 0 leads >= ${failOnCandidates ?? "disabled"}; coverage ${report4.coverageStatus}. This is not proof that the application is secure.`);
|
|
9172
9482
|
}
|
|
9173
9483
|
}
|
|
9174
9484
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -9251,17 +9561,17 @@ async function runHostedSecurity(options) {
|
|
|
9251
9561
|
allowNetwork: false
|
|
9252
9562
|
}
|
|
9253
9563
|
});
|
|
9254
|
-
const
|
|
9255
|
-
await (0, import_node3.writeSecurityArtifacts)(output,
|
|
9256
|
-
const reportDigest = await (0, import_security.securityFingerprint)(
|
|
9564
|
+
const report4 = await harness.run(snapshot, { runId: hosted.run.runId, signal: options.signal });
|
|
9565
|
+
await (0, import_node3.writeSecurityArtifacts)(output, report4);
|
|
9566
|
+
const reportDigest = await (0, import_security.securityFingerprint)(report4);
|
|
9257
9567
|
await hosted.complete({
|
|
9258
9568
|
reportDigest,
|
|
9259
|
-
coverageStatus:
|
|
9260
|
-
confirmed:
|
|
9261
|
-
candidates:
|
|
9569
|
+
coverageStatus: report4.coverageStatus,
|
|
9570
|
+
confirmed: report4.metrics.confirmed,
|
|
9571
|
+
candidates: report4.metrics.candidates
|
|
9262
9572
|
}, { signal: options.signal });
|
|
9263
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
9264
|
-
return Object.freeze({ report:
|
|
9573
|
+
printSummary(options.stdout ?? console, appId, env, hosted.run, report4, output);
|
|
9574
|
+
return Object.freeze({ report: report4, run: hosted.run, output });
|
|
9265
9575
|
}
|
|
9266
9576
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
9267
9577
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -9271,11 +9581,11 @@ function selectEnv(requested, declared, configPath, rootDir) {
|
|
|
9271
9581
|
}
|
|
9272
9582
|
return env;
|
|
9273
9583
|
}
|
|
9274
|
-
async function injectedToken(options,
|
|
9275
|
-
const value = options.token ?? await options.getToken?.(Object.freeze({ ...
|
|
9584
|
+
async function injectedToken(options, request2) {
|
|
9585
|
+
const value = options.token ?? await options.getToken?.(Object.freeze({ ...request2 }));
|
|
9276
9586
|
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
9277
9587
|
throw new Error(
|
|
9278
|
-
|
|
9588
|
+
request2.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
|
|
9279
9589
|
);
|
|
9280
9590
|
}
|
|
9281
9591
|
return value;
|
|
@@ -9287,14 +9597,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
9287
9597
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
9288
9598
|
return { ...profile, maxHuntTasks };
|
|
9289
9599
|
}
|
|
9290
|
-
function printSummary(out, appId, env, run,
|
|
9291
|
-
const complete =
|
|
9600
|
+
function printSummary(out, appId, env, run, report4, output) {
|
|
9601
|
+
const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
|
|
9292
9602
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
9293
9603
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
9294
9604
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
9295
|
-
out.log(` coverage: ${
|
|
9296
|
-
if (
|
|
9297
|
-
out.log(` findings: confirmed=${
|
|
9605
|
+
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}`);
|
|
9606
|
+
if (report4.callBudget) out.log(` calls: discovery=${formatBudget(report4.callBudget.discovery)} validation=${formatBudget(report4.callBudget.validation)}`);
|
|
9607
|
+
out.log(` findings: confirmed=${report4.metrics.confirmed} needs_reproduction=${report4.metrics.needsReproduction} candidates=${report4.metrics.candidates}`);
|
|
9298
9608
|
out.log(` report: ${(0, import_node_path15.resolve)(output, "REPORT.md")}`);
|
|
9299
9609
|
}
|
|
9300
9610
|
function formatBudget(usage) {
|
|
@@ -9541,13 +9851,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
9541
9851
|
}
|
|
9542
9852
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
9543
9853
|
}
|
|
9544
|
-
const
|
|
9854
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
9545
9855
|
if (parsed.options.json === true) {
|
|
9546
|
-
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report:
|
|
9856
|
+
context.stdout.log(JSON.stringify({ plan, intent: preview.intent, job: result, report: report4 }, null, 2));
|
|
9547
9857
|
} else {
|
|
9548
|
-
printHostedReport(context.stdout,
|
|
9858
|
+
printHostedReport(context.stdout, report4);
|
|
9549
9859
|
}
|
|
9550
|
-
enforceHostedReportGate(
|
|
9860
|
+
enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
|
|
9551
9861
|
}
|
|
9552
9862
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
9553
9863
|
if (parsed.options.source === true) {
|
|
@@ -9588,11 +9898,11 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9588
9898
|
sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
|
|
9589
9899
|
fetch: doFetch,
|
|
9590
9900
|
stdout: out,
|
|
9591
|
-
getToken: async (
|
|
9592
|
-
if (
|
|
9901
|
+
getToken: async (request2) => {
|
|
9902
|
+
if (request2.scope === "platform:security:self") {
|
|
9593
9903
|
return getScopedPlatformToken({
|
|
9594
|
-
platform:
|
|
9595
|
-
scope:
|
|
9904
|
+
platform: request2.platform,
|
|
9905
|
+
scope: request2.scope,
|
|
9596
9906
|
email: stringOpt(parsed.options.email),
|
|
9597
9907
|
open,
|
|
9598
9908
|
fetch: doFetch,
|
|
@@ -9601,7 +9911,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9601
9911
|
});
|
|
9602
9912
|
}
|
|
9603
9913
|
const cfg = await loadProjectConfig(configPath);
|
|
9604
|
-
if (platformAudience(cfg.platformUrl) !== platformAudience(
|
|
9914
|
+
if (platformAudience(cfg.platformUrl) !== platformAudience(request2.platform)) {
|
|
9605
9915
|
throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
|
|
9606
9916
|
}
|
|
9607
9917
|
return getDeveloperToken(
|
|
@@ -9614,13 +9924,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9614
9924
|
});
|
|
9615
9925
|
enforceLocalGate(result.report, parsed);
|
|
9616
9926
|
}
|
|
9617
|
-
function enforceLocalGate(
|
|
9927
|
+
function enforceLocalGate(report4, parsed) {
|
|
9618
9928
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
9619
9929
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
9620
9930
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
9621
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
9622
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
9623
|
-
const incomplete =
|
|
9931
|
+
const confirmed = (0, import_security2.findingsAtOrAbove)(report4, failOn);
|
|
9932
|
+
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(report4, failOnCandidates, true).filter((finding) => finding.disposition !== "confirmed") : [];
|
|
9933
|
+
const incomplete = report4.coverageStatus === "incomplete" && parsed.options["allow-incomplete"] !== true;
|
|
9624
9934
|
if (confirmed.length || leads.length || incomplete) {
|
|
9625
9935
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
9626
9936
|
}
|
|
@@ -9659,9 +9969,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
9659
9969
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
9660
9970
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
9661
9971
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
9662
|
-
const
|
|
9663
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
9664
|
-
else printHostedReport(context.stdout,
|
|
9972
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId });
|
|
9973
|
+
if (parsed.options.json === true) context.stdout.log(JSON.stringify(report4, null, 2));
|
|
9974
|
+
else printHostedReport(context.stdout, report4);
|
|
9665
9975
|
return;
|
|
9666
9976
|
}
|
|
9667
9977
|
if (sub !== "run") {
|
|
@@ -9745,7 +10055,8 @@ async function securityStatus(parsed, dependencies) {
|
|
|
9745
10055
|
|
|
9746
10056
|
// src/cli.ts
|
|
9747
10057
|
function exitCodeFor(err) {
|
|
9748
|
-
|
|
10058
|
+
const code = err?.code;
|
|
10059
|
+
return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
|
|
9749
10060
|
}
|
|
9750
10061
|
async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
9751
10062
|
const parsed = parseArgv(argv);
|
|
@@ -9793,6 +10104,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
9793
10104
|
await pmCommand(parsed, dependencies);
|
|
9794
10105
|
return;
|
|
9795
10106
|
}
|
|
10107
|
+
if (command === "discuss") {
|
|
10108
|
+
await discussCommand(parsed, dependencies);
|
|
10109
|
+
return;
|
|
10110
|
+
}
|
|
9796
10111
|
if (command === "provision") {
|
|
9797
10112
|
await provisionCommand(parsed, dependencies);
|
|
9798
10113
|
return;
|