@odla-ai/cli 0.23.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/README.md +8 -0
- package/dist/bin.cjs +582 -185
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-RZDRUVBT.js → chunk-HG63GLGU.js} +583 -186
- package/dist/chunk-HG63GLGU.js.map +1 -0
- package/dist/index.cjs +582 -185
- 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-RZDRUVBT.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
|
}
|
|
@@ -6426,13 +6426,22 @@ Usage:
|
|
|
6426
6426
|
odla-ai app owners add <email> [--email <odla-account>] [--json]
|
|
6427
6427
|
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
6428
6428
|
odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
|
|
6429
|
-
odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--body <text>|--proof <text>]
|
|
6429
|
+
odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>]
|
|
6430
6430
|
odla-ai pm <goal|task|decision|bug> get <id> [--json]
|
|
6431
|
-
odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>]
|
|
6432
|
-
odla-ai pm <goal|task|decision|bug> done <id>
|
|
6431
|
+
odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>]
|
|
6432
|
+
odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>]
|
|
6433
6433
|
odla-ai pm <goal|task|decision|bug> comment <id> --body "..."
|
|
6434
6434
|
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
6435
6435
|
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
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]
|
|
6436
6445
|
odla-ai whoami [--json]
|
|
6437
6446
|
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
6438
6447
|
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
@@ -6537,6 +6546,13 @@ Commands:
|
|
|
6537
6546
|
"app". Entities: goal (alias conformance), task (alias kanban),
|
|
6538
6547
|
decision, bug. Status changes and comments post to each item's
|
|
6539
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.
|
|
6540
6556
|
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
6541
6557
|
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
6542
6558
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
@@ -6724,15 +6740,15 @@ async function connectGitHubSecuritySource(options) {
|
|
|
6724
6740
|
const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
|
|
6725
6741
|
const wait2 = options.wait ?? waitForHostedPoll;
|
|
6726
6742
|
while (true) {
|
|
6727
|
-
const
|
|
6743
|
+
const state2 = await requestHostedSecurityJson(
|
|
6728
6744
|
options,
|
|
6729
6745
|
`/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
|
|
6730
6746
|
{},
|
|
6731
6747
|
"check GitHub connection"
|
|
6732
6748
|
);
|
|
6733
|
-
if (
|
|
6734
|
-
if (
|
|
6735
|
-
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}` : ""}`);
|
|
6736
6752
|
}
|
|
6737
6753
|
if (now() >= deadline) throw new Error("GitHub connection approval timed out");
|
|
6738
6754
|
await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
@@ -7183,6 +7199,289 @@ async function codeCommand(parsed, dependencies) {
|
|
|
7183
7199
|
});
|
|
7184
7200
|
}
|
|
7185
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
|
+
|
|
7186
7485
|
// src/pm-actions.ts
|
|
7187
7486
|
var FIELD_MAP = {
|
|
7188
7487
|
title: { key: "title" },
|
|
@@ -7191,6 +7490,7 @@ var FIELD_MAP = {
|
|
|
7191
7490
|
column: { key: "column" },
|
|
7192
7491
|
rank: { key: "rank", num: true },
|
|
7193
7492
|
goal: { key: "goalId" },
|
|
7493
|
+
decision: { key: "decisionId" },
|
|
7194
7494
|
assignee: { key: "assigneeId" },
|
|
7195
7495
|
due: { key: "dueAt", num: true },
|
|
7196
7496
|
proof: { key: "proof" },
|
|
@@ -7229,6 +7529,14 @@ function collectFields(parsed, allowClear) {
|
|
|
7229
7529
|
}
|
|
7230
7530
|
return out;
|
|
7231
7531
|
}
|
|
7532
|
+
function collectEntityFields(entity, parsed, allowClear) {
|
|
7533
|
+
const fields = collectFields(parsed, allowClear);
|
|
7534
|
+
if ((entity === "task" || entity === "bug") && fields.body !== void 0) {
|
|
7535
|
+
if (fields.description === void 0) fields.description = fields.body;
|
|
7536
|
+
delete fields.body;
|
|
7537
|
+
}
|
|
7538
|
+
return fields;
|
|
7539
|
+
}
|
|
7232
7540
|
function statusCol(entity, r) {
|
|
7233
7541
|
if (entity === "bug") return `${r.status ?? ""}/${r.severity ?? ""}`;
|
|
7234
7542
|
if (entity === "task") return String(r.column ?? "");
|
|
@@ -7237,13 +7545,20 @@ function statusCol(entity, r) {
|
|
|
7237
7545
|
function printRecord(ctx, entity, r) {
|
|
7238
7546
|
ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
|
|
7239
7547
|
}
|
|
7240
|
-
function
|
|
7548
|
+
function emit2(ctx, value, human) {
|
|
7241
7549
|
if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
|
|
7242
7550
|
else human();
|
|
7243
7551
|
}
|
|
7244
7552
|
async function pmList(ctx, entity, parsed) {
|
|
7245
7553
|
const q = new URLSearchParams();
|
|
7246
|
-
const filters = {
|
|
7554
|
+
const filters = {
|
|
7555
|
+
status: "status",
|
|
7556
|
+
severity: "severity",
|
|
7557
|
+
column: "column",
|
|
7558
|
+
goal: "goalId",
|
|
7559
|
+
assignee: "assigneeId",
|
|
7560
|
+
decision: "decisionId"
|
|
7561
|
+
};
|
|
7247
7562
|
const app = stringOpt(parsed.options.app);
|
|
7248
7563
|
if (app) q.set("app", app);
|
|
7249
7564
|
for (const [flag, param] of Object.entries(filters)) {
|
|
@@ -7256,7 +7571,7 @@ async function pmList(ctx, entity, parsed) {
|
|
|
7256
7571
|
}
|
|
7257
7572
|
const qs = q.toString();
|
|
7258
7573
|
const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
|
|
7259
|
-
|
|
7574
|
+
emit2(ctx, page, () => {
|
|
7260
7575
|
ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
|
|
7261
7576
|
ctx.out.log("id state app title");
|
|
7262
7577
|
for (const r of page.records) printRecord(ctx, entity, r);
|
|
@@ -7265,25 +7580,82 @@ async function pmList(ctx, entity, parsed) {
|
|
|
7265
7580
|
async function pmAdd(ctx, entity, parsed) {
|
|
7266
7581
|
const appId = stringOpt(parsed.options.app);
|
|
7267
7582
|
if (!appId) throw new Error("pm add needs --app <appId>");
|
|
7268
|
-
const input =
|
|
7583
|
+
const input = collectEntityFields(entity, parsed, false);
|
|
7269
7584
|
if (!input.title) throw new Error("pm add needs --title <title>");
|
|
7585
|
+
if (entity === "bug" && !input.description)
|
|
7586
|
+
throw new Error("pm bug add needs --description <context> (or --body <context>)");
|
|
7270
7587
|
const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
|
|
7271
|
-
|
|
7588
|
+
emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
|
|
7272
7589
|
}
|
|
7273
7590
|
async function pmGet(ctx, entity, id) {
|
|
7274
7591
|
const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
|
|
7275
|
-
|
|
7592
|
+
emit2(ctx, record5, () => printRecord(ctx, entity, record5));
|
|
7276
7593
|
}
|
|
7277
7594
|
async function pmSet(ctx, entity, id, parsed) {
|
|
7278
|
-
const patch2 =
|
|
7595
|
+
const patch2 = collectEntityFields(entity, parsed, true);
|
|
7279
7596
|
if (Object.keys(patch2).length === 0)
|
|
7280
7597
|
throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
|
|
7281
7598
|
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
7282
|
-
|
|
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}`));
|
|
7283
7600
|
}
|
|
7284
|
-
async function pmDone(ctx, entity, id) {
|
|
7285
|
-
const
|
|
7286
|
-
|
|
7601
|
+
async function pmDone(ctx, entity, id, parsed) {
|
|
7602
|
+
const decisionId = stringOpt(parsed.options.decision);
|
|
7603
|
+
if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
|
|
7604
|
+
const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
|
|
7605
|
+
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
7606
|
+
emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
|
|
7607
|
+
}
|
|
7608
|
+
async function allRecords(ctx, entity, appId) {
|
|
7609
|
+
const records = [];
|
|
7610
|
+
for (; ; ) {
|
|
7611
|
+
const q = new URLSearchParams({
|
|
7612
|
+
app: appId,
|
|
7613
|
+
limit: "100",
|
|
7614
|
+
offset: String(records.length)
|
|
7615
|
+
});
|
|
7616
|
+
const page = await pmRequest(ctx, "GET", `/${entity}?${q}`);
|
|
7617
|
+
records.push(...page.records);
|
|
7618
|
+
if (records.length >= page.total || page.records.length === 0) return records;
|
|
7619
|
+
}
|
|
7620
|
+
}
|
|
7621
|
+
async function pmHandoff(ctx, parsed) {
|
|
7622
|
+
const appId = stringOpt(parsed.options.app);
|
|
7623
|
+
if (!appId) throw new Error("pm handoff needs --app <appId>");
|
|
7624
|
+
const [goals, tasks, bugs] = await Promise.all([
|
|
7625
|
+
allRecords(ctx, "goal", appId),
|
|
7626
|
+
allRecords(ctx, "task", appId),
|
|
7627
|
+
allRecords(ctx, "bug", appId)
|
|
7628
|
+
]);
|
|
7629
|
+
const handoff = {
|
|
7630
|
+
appId,
|
|
7631
|
+
unmetGoals: goals.filter((record5) => record5.status !== "met"),
|
|
7632
|
+
activeTasks: tasks.filter((record5) => record5.column !== "done"),
|
|
7633
|
+
openBugs: bugs.filter((record5) => record5.status !== "fixed" && record5.status !== "wontfix")
|
|
7634
|
+
};
|
|
7635
|
+
const result = {
|
|
7636
|
+
...handoff,
|
|
7637
|
+
clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
|
|
7638
|
+
};
|
|
7639
|
+
emit2(ctx, result, () => {
|
|
7640
|
+
if (result.clean) {
|
|
7641
|
+
ctx.out.log(`${appId}: no unresolved PM work`);
|
|
7642
|
+
return;
|
|
7643
|
+
}
|
|
7644
|
+
ctx.out.log(`${appId}: authoritative PM handoff`);
|
|
7645
|
+
for (const [label, records] of [
|
|
7646
|
+
["unmet goals", result.unmetGoals],
|
|
7647
|
+
["active tasks", result.activeTasks],
|
|
7648
|
+
["open bugs", result.openBugs]
|
|
7649
|
+
]) {
|
|
7650
|
+
ctx.out.log(`${label}:`);
|
|
7651
|
+
if (!records.length) ctx.out.log("- (none)");
|
|
7652
|
+
else for (const record5 of records) printRecord(
|
|
7653
|
+
ctx,
|
|
7654
|
+
label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
|
|
7655
|
+
record5
|
|
7656
|
+
);
|
|
7657
|
+
}
|
|
7658
|
+
});
|
|
7287
7659
|
}
|
|
7288
7660
|
async function pmComment(ctx, entity, id, parsed) {
|
|
7289
7661
|
const body = stringOpt(parsed.options.body);
|
|
@@ -7297,7 +7669,7 @@ async function pmComments(ctx, entity, id) {
|
|
|
7297
7669
|
"GET",
|
|
7298
7670
|
`/${entity}/${encodeURIComponent(id)}/comments`
|
|
7299
7671
|
);
|
|
7300
|
-
|
|
7672
|
+
emit2(ctx, messages, () => {
|
|
7301
7673
|
if (messages.length === 0) ctx.out.log("(no comments)");
|
|
7302
7674
|
else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
|
|
7303
7675
|
});
|
|
@@ -7316,7 +7688,7 @@ var ALIASES = {
|
|
|
7316
7688
|
decision: "decision",
|
|
7317
7689
|
bug: "bug"
|
|
7318
7690
|
};
|
|
7319
|
-
var
|
|
7691
|
+
var ALLOWED2 = [
|
|
7320
7692
|
"config",
|
|
7321
7693
|
"token",
|
|
7322
7694
|
"email",
|
|
@@ -7335,15 +7707,16 @@ var ALLOWED = [
|
|
|
7335
7707
|
"target",
|
|
7336
7708
|
"description",
|
|
7337
7709
|
"desc",
|
|
7710
|
+
"decision",
|
|
7338
7711
|
"limit",
|
|
7339
7712
|
"offset",
|
|
7340
7713
|
"q"
|
|
7341
7714
|
];
|
|
7342
|
-
function
|
|
7715
|
+
function requireId2(id, action) {
|
|
7343
7716
|
if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
|
|
7344
7717
|
return id;
|
|
7345
7718
|
}
|
|
7346
|
-
async function
|
|
7719
|
+
async function buildContext2(parsed, deps) {
|
|
7347
7720
|
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
7348
7721
|
const doFetch = deps.fetch ?? fetch;
|
|
7349
7722
|
const out = deps.stdout ?? console;
|
|
@@ -7357,11 +7730,15 @@ async function buildContext(parsed, deps) {
|
|
|
7357
7730
|
}
|
|
7358
7731
|
async function pmCommand(parsed, deps = {}) {
|
|
7359
7732
|
const word = parsed.positionals[1] ?? "";
|
|
7733
|
+
if (word === "handoff") {
|
|
7734
|
+
assertArgs(parsed, ALLOWED2, 2);
|
|
7735
|
+
return pmHandoff(await buildContext2(parsed, deps), parsed);
|
|
7736
|
+
}
|
|
7360
7737
|
const entity = ALIASES[word];
|
|
7361
7738
|
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
7362
7739
|
const action = parsed.positionals[2] ?? "list";
|
|
7363
|
-
assertArgs(parsed,
|
|
7364
|
-
const ctx = await
|
|
7740
|
+
assertArgs(parsed, ALLOWED2, 4);
|
|
7741
|
+
const ctx = await buildContext2(parsed, deps);
|
|
7365
7742
|
const id = parsed.positionals[3];
|
|
7366
7743
|
switch (action) {
|
|
7367
7744
|
case "list":
|
|
@@ -7370,21 +7747,21 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
7370
7747
|
case "create":
|
|
7371
7748
|
return pmAdd(ctx, entity, parsed);
|
|
7372
7749
|
case "get":
|
|
7373
|
-
return pmGet(ctx, entity,
|
|
7750
|
+
return pmGet(ctx, entity, requireId2(id, action));
|
|
7374
7751
|
case "set":
|
|
7375
7752
|
case "update":
|
|
7376
7753
|
case "status":
|
|
7377
7754
|
case "move":
|
|
7378
|
-
return pmSet(ctx, entity,
|
|
7755
|
+
return pmSet(ctx, entity, requireId2(id, action), parsed);
|
|
7379
7756
|
case "done":
|
|
7380
|
-
return pmDone(ctx, entity,
|
|
7757
|
+
return pmDone(ctx, entity, requireId2(id, action), parsed);
|
|
7381
7758
|
case "comment":
|
|
7382
|
-
return pmComment(ctx, entity,
|
|
7759
|
+
return pmComment(ctx, entity, requireId2(id, action), parsed);
|
|
7383
7760
|
case "comments":
|
|
7384
|
-
return pmComments(ctx, entity,
|
|
7761
|
+
return pmComments(ctx, entity, requireId2(id, action));
|
|
7385
7762
|
case "rm":
|
|
7386
7763
|
case "delete":
|
|
7387
|
-
return pmRemove(ctx, entity,
|
|
7764
|
+
return pmRemove(ctx, entity, requireId2(id, action));
|
|
7388
7765
|
default:
|
|
7389
7766
|
throw new Error(`unknown pm action "${action}". Try list|add|get|set|done|comment|comments|rm.`);
|
|
7390
7767
|
}
|
|
@@ -7850,10 +8227,25 @@ var COMMAND_SURFACE = {
|
|
|
7850
8227
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
7851
8228
|
capabilities: {},
|
|
7852
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
|
+
},
|
|
7853
8242
|
doctor: {},
|
|
7854
8243
|
help: {},
|
|
7855
8244
|
init: {},
|
|
7856
|
-
pm:
|
|
8245
|
+
pm: {
|
|
8246
|
+
...PM_ENTITIES,
|
|
8247
|
+
handoff: {}
|
|
8248
|
+
},
|
|
7857
8249
|
provision: {},
|
|
7858
8250
|
runbook: {
|
|
7859
8251
|
ask: {},
|
|
@@ -8434,7 +8826,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
8434
8826
|
return out;
|
|
8435
8827
|
}
|
|
8436
8828
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
8437
|
-
function
|
|
8829
|
+
function report3(ctx, impacts) {
|
|
8438
8830
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
8439
8831
|
ctx.out.log(
|
|
8440
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.`
|
|
@@ -8472,7 +8864,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
8472
8864
|
}
|
|
8473
8865
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
8474
8866
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
8475
|
-
|
|
8867
|
+
report3(ctx, impacts);
|
|
8476
8868
|
}
|
|
8477
8869
|
|
|
8478
8870
|
// src/runbook-lint.ts
|
|
@@ -8705,7 +9097,7 @@ async function whoamiCommand(parsed, deps = {}) {
|
|
|
8705
9097
|
}
|
|
8706
9098
|
|
|
8707
9099
|
// src/runbook-command.ts
|
|
8708
|
-
var
|
|
9100
|
+
var ALLOWED3 = [
|
|
8709
9101
|
"config",
|
|
8710
9102
|
"token",
|
|
8711
9103
|
"email",
|
|
@@ -8745,7 +9137,7 @@ async function loadOrDefaultConfig(configPath, action, appId) {
|
|
|
8745
9137
|
}
|
|
8746
9138
|
return loadProjectConfig(configPath);
|
|
8747
9139
|
}
|
|
8748
|
-
async function
|
|
9140
|
+
async function buildContext3(parsed, deps, action) {
|
|
8749
9141
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
8750
9142
|
const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
|
|
8751
9143
|
const doFetch = deps.fetch ?? fetch;
|
|
@@ -8801,8 +9193,8 @@ async function buildContext2(parsed, deps, action) {
|
|
|
8801
9193
|
}
|
|
8802
9194
|
async function runbookCommand(parsed, deps = {}) {
|
|
8803
9195
|
const action = parsed.positionals[1] ?? "list";
|
|
8804
|
-
assertArgs(parsed,
|
|
8805
|
-
const ctx = await
|
|
9196
|
+
assertArgs(parsed, ALLOWED3, action === "ask" || action === "search" ? 64 : 4);
|
|
9197
|
+
const ctx = await buildContext3(parsed, deps, action);
|
|
8806
9198
|
const slug = parsed.positionals[2];
|
|
8807
9199
|
switch (action) {
|
|
8808
9200
|
case "list":
|
|
@@ -8993,31 +9385,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
8993
9385
|
url.searchParams.set("job", job.jobId);
|
|
8994
9386
|
out.log(` Studio: ${url.toString()}`);
|
|
8995
9387
|
}
|
|
8996
|
-
function printHostedReport(out,
|
|
8997
|
-
out.log(`security report ${
|
|
8998
|
-
out.log(` coverage: ${
|
|
8999
|
-
out.log(` findings: confirmed=${
|
|
9000
|
-
out.log(` discovery: ${
|
|
9001
|
-
out.log(` validation: ${
|
|
9002
|
-
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) {
|
|
9003
9395
|
const location = finding.locations[0];
|
|
9004
9396
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
9005
9397
|
}
|
|
9006
|
-
for (const limitation of
|
|
9398
|
+
for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
|
|
9007
9399
|
}
|
|
9008
|
-
function enforceHostedReportGate(
|
|
9400
|
+
function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
|
|
9009
9401
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
9010
9402
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
9011
9403
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
9012
9404
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
9013
|
-
const confirmed =
|
|
9014
|
-
const leads = failOnCandidates ?
|
|
9015
|
-
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;
|
|
9016
9408
|
if (confirmed.length || leads.length || incomplete) {
|
|
9017
|
-
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}` : ""}`);
|
|
9018
9410
|
}
|
|
9019
9411
|
if (emitSuccess) {
|
|
9020
|
-
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.`);
|
|
9021
9413
|
}
|
|
9022
9414
|
}
|
|
9023
9415
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -9100,17 +9492,17 @@ async function runHostedSecurity(options) {
|
|
|
9100
9492
|
allowNetwork: false
|
|
9101
9493
|
}
|
|
9102
9494
|
});
|
|
9103
|
-
const
|
|
9104
|
-
await (0, import_node3.writeSecurityArtifacts)(output,
|
|
9105
|
-
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);
|
|
9106
9498
|
await hosted.complete({
|
|
9107
9499
|
reportDigest,
|
|
9108
|
-
coverageStatus:
|
|
9109
|
-
confirmed:
|
|
9110
|
-
candidates:
|
|
9500
|
+
coverageStatus: report4.coverageStatus,
|
|
9501
|
+
confirmed: report4.metrics.confirmed,
|
|
9502
|
+
candidates: report4.metrics.candidates
|
|
9111
9503
|
}, { signal: options.signal });
|
|
9112
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
9113
|
-
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 });
|
|
9114
9506
|
}
|
|
9115
9507
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
9116
9508
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -9120,11 +9512,11 @@ function selectEnv(requested, declared, configPath, rootDir) {
|
|
|
9120
9512
|
}
|
|
9121
9513
|
return env;
|
|
9122
9514
|
}
|
|
9123
|
-
async function injectedToken(options,
|
|
9124
|
-
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 }));
|
|
9125
9517
|
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
9126
9518
|
throw new Error(
|
|
9127
|
-
|
|
9519
|
+
request2.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
|
|
9128
9520
|
);
|
|
9129
9521
|
}
|
|
9130
9522
|
return value;
|
|
@@ -9136,14 +9528,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
9136
9528
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
9137
9529
|
return { ...profile, maxHuntTasks };
|
|
9138
9530
|
}
|
|
9139
|
-
function printSummary(out, appId, env, run,
|
|
9140
|
-
const complete =
|
|
9531
|
+
function printSummary(out, appId, env, run, report4, output) {
|
|
9532
|
+
const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
|
|
9141
9533
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
9142
9534
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
9143
9535
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
9144
|
-
out.log(` coverage: ${
|
|
9145
|
-
if (
|
|
9146
|
-
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}`);
|
|
9147
9539
|
out.log(` report: ${(0, import_node_path15.resolve)(output, "REPORT.md")}`);
|
|
9148
9540
|
}
|
|
9149
9541
|
function formatBudget(usage) {
|
|
@@ -9379,13 +9771,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
9379
9771
|
}
|
|
9380
9772
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
9381
9773
|
}
|
|
9382
|
-
const
|
|
9774
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
9383
9775
|
if (parsed.options.json === true) {
|
|
9384
|
-
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));
|
|
9385
9777
|
} else {
|
|
9386
|
-
printHostedReport(context.stdout,
|
|
9778
|
+
printHostedReport(context.stdout, report4);
|
|
9387
9779
|
}
|
|
9388
|
-
enforceHostedReportGate(
|
|
9780
|
+
enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
|
|
9389
9781
|
}
|
|
9390
9782
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
9391
9783
|
if (parsed.options.source === true) {
|
|
@@ -9426,11 +9818,11 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9426
9818
|
sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
|
|
9427
9819
|
fetch: doFetch,
|
|
9428
9820
|
stdout: out,
|
|
9429
|
-
getToken: async (
|
|
9430
|
-
if (
|
|
9821
|
+
getToken: async (request2) => {
|
|
9822
|
+
if (request2.scope === "platform:security:self") {
|
|
9431
9823
|
return getScopedPlatformToken({
|
|
9432
|
-
platform:
|
|
9433
|
-
scope:
|
|
9824
|
+
platform: request2.platform,
|
|
9825
|
+
scope: request2.scope,
|
|
9434
9826
|
email: stringOpt(parsed.options.email),
|
|
9435
9827
|
open,
|
|
9436
9828
|
fetch: doFetch,
|
|
@@ -9439,7 +9831,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9439
9831
|
});
|
|
9440
9832
|
}
|
|
9441
9833
|
const cfg = await loadProjectConfig(configPath);
|
|
9442
|
-
if (platformAudience(cfg.platformUrl) !== platformAudience(
|
|
9834
|
+
if (platformAudience(cfg.platformUrl) !== platformAudience(request2.platform)) {
|
|
9443
9835
|
throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
|
|
9444
9836
|
}
|
|
9445
9837
|
return getDeveloperToken(
|
|
@@ -9452,13 +9844,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9452
9844
|
});
|
|
9453
9845
|
enforceLocalGate(result.report, parsed);
|
|
9454
9846
|
}
|
|
9455
|
-
function enforceLocalGate(
|
|
9847
|
+
function enforceLocalGate(report4, parsed) {
|
|
9456
9848
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
9457
9849
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
9458
9850
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
9459
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
9460
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
9461
|
-
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;
|
|
9462
9854
|
if (confirmed.length || leads.length || incomplete) {
|
|
9463
9855
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
9464
9856
|
}
|
|
@@ -9497,9 +9889,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
9497
9889
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
9498
9890
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
9499
9891
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
9500
|
-
const
|
|
9501
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
9502
|
-
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);
|
|
9503
9895
|
return;
|
|
9504
9896
|
}
|
|
9505
9897
|
if (sub !== "run") {
|
|
@@ -9583,7 +9975,8 @@ async function securityStatus(parsed, dependencies) {
|
|
|
9583
9975
|
|
|
9584
9976
|
// src/cli.ts
|
|
9585
9977
|
function exitCodeFor(err) {
|
|
9586
|
-
|
|
9978
|
+
const code = err?.code;
|
|
9979
|
+
return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
|
|
9587
9980
|
}
|
|
9588
9981
|
async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
9589
9982
|
const parsed = parseArgv(argv);
|
|
@@ -9631,6 +10024,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
9631
10024
|
await pmCommand(parsed, dependencies);
|
|
9632
10025
|
return;
|
|
9633
10026
|
}
|
|
10027
|
+
if (command === "discuss") {
|
|
10028
|
+
await discussCommand(parsed, dependencies);
|
|
10029
|
+
return;
|
|
10030
|
+
}
|
|
9634
10031
|
if (command === "provision") {
|
|
9635
10032
|
await provisionCommand(parsed, dependencies);
|
|
9636
10033
|
return;
|