@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/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
|
}
|
|
@@ -6486,13 +6486,22 @@ Usage:
|
|
|
6486
6486
|
odla-ai app owners add <email> [--email <odla-account>] [--json]
|
|
6487
6487
|
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
6488
6488
|
odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
|
|
6489
|
-
odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--body <text>|--proof <text>]
|
|
6489
|
+
odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>]
|
|
6490
6490
|
odla-ai pm <goal|task|decision|bug> get <id> [--json]
|
|
6491
|
-
odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>]
|
|
6492
|
-
odla-ai pm <goal|task|decision|bug> done <id>
|
|
6491
|
+
odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>]
|
|
6492
|
+
odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>]
|
|
6493
6493
|
odla-ai pm <goal|task|decision|bug> comment <id> --body "..."
|
|
6494
6494
|
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
6495
6495
|
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
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]
|
|
6496
6505
|
odla-ai whoami [--json]
|
|
6497
6506
|
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
6498
6507
|
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
@@ -6597,6 +6606,13 @@ Commands:
|
|
|
6597
6606
|
"app". Entities: goal (alias conformance), task (alias kanban),
|
|
6598
6607
|
decision, bug. Status changes and comments post to each item's
|
|
6599
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.
|
|
6600
6616
|
provision Register services, compose integrations, persist credentials, optionally push secrets.
|
|
6601
6617
|
smoke Verify credentials, public-config, composed schema, db aggregate, and integration probes.
|
|
6602
6618
|
skill Same installer; --agent accepts all, claude, codex, cursor,
|
|
@@ -6784,15 +6800,15 @@ async function connectGitHubSecuritySource(options) {
|
|
|
6784
6800
|
const deadline = Math.min(serverExpiry, now() + hostedPollTimeout(options.pollTimeoutMs));
|
|
6785
6801
|
const wait2 = options.wait ?? waitForHostedPoll;
|
|
6786
6802
|
while (true) {
|
|
6787
|
-
const
|
|
6803
|
+
const state2 = await requestHostedSecurityJson(
|
|
6788
6804
|
options,
|
|
6789
6805
|
`/registry/github/connect/${encodeURIComponent(attempt.attemptId)}`,
|
|
6790
6806
|
{},
|
|
6791
6807
|
"check GitHub connection"
|
|
6792
6808
|
);
|
|
6793
|
-
if (
|
|
6794
|
-
if (
|
|
6795
|
-
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}` : ""}`);
|
|
6796
6812
|
}
|
|
6797
6813
|
if (now() >= deadline) throw new Error("GitHub connection approval timed out");
|
|
6798
6814
|
await wait2(Math.min(interval, Math.max(0, deadline - now())), options.signal);
|
|
@@ -7243,6 +7259,289 @@ async function codeCommand(parsed, dependencies) {
|
|
|
7243
7259
|
});
|
|
7244
7260
|
}
|
|
7245
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
|
+
|
|
7246
7545
|
// src/pm-actions.ts
|
|
7247
7546
|
var FIELD_MAP = {
|
|
7248
7547
|
title: { key: "title" },
|
|
@@ -7251,6 +7550,7 @@ var FIELD_MAP = {
|
|
|
7251
7550
|
column: { key: "column" },
|
|
7252
7551
|
rank: { key: "rank", num: true },
|
|
7253
7552
|
goal: { key: "goalId" },
|
|
7553
|
+
decision: { key: "decisionId" },
|
|
7254
7554
|
assignee: { key: "assigneeId" },
|
|
7255
7555
|
due: { key: "dueAt", num: true },
|
|
7256
7556
|
proof: { key: "proof" },
|
|
@@ -7289,6 +7589,14 @@ function collectFields(parsed, allowClear) {
|
|
|
7289
7589
|
}
|
|
7290
7590
|
return out;
|
|
7291
7591
|
}
|
|
7592
|
+
function collectEntityFields(entity, parsed, allowClear) {
|
|
7593
|
+
const fields = collectFields(parsed, allowClear);
|
|
7594
|
+
if ((entity === "task" || entity === "bug") && fields.body !== void 0) {
|
|
7595
|
+
if (fields.description === void 0) fields.description = fields.body;
|
|
7596
|
+
delete fields.body;
|
|
7597
|
+
}
|
|
7598
|
+
return fields;
|
|
7599
|
+
}
|
|
7292
7600
|
function statusCol(entity, r) {
|
|
7293
7601
|
if (entity === "bug") return `${r.status ?? ""}/${r.severity ?? ""}`;
|
|
7294
7602
|
if (entity === "task") return String(r.column ?? "");
|
|
@@ -7297,13 +7605,20 @@ function statusCol(entity, r) {
|
|
|
7297
7605
|
function printRecord(ctx, entity, r) {
|
|
7298
7606
|
ctx.out.log(`${r.id} [${statusCol(entity, r)}] ${r.appId} ${r.title ?? ""}`);
|
|
7299
7607
|
}
|
|
7300
|
-
function
|
|
7608
|
+
function emit2(ctx, value, human) {
|
|
7301
7609
|
if (ctx.json) ctx.out.log(JSON.stringify(value, null, 2));
|
|
7302
7610
|
else human();
|
|
7303
7611
|
}
|
|
7304
7612
|
async function pmList(ctx, entity, parsed) {
|
|
7305
7613
|
const q = new URLSearchParams();
|
|
7306
|
-
const filters = {
|
|
7614
|
+
const filters = {
|
|
7615
|
+
status: "status",
|
|
7616
|
+
severity: "severity",
|
|
7617
|
+
column: "column",
|
|
7618
|
+
goal: "goalId",
|
|
7619
|
+
assignee: "assigneeId",
|
|
7620
|
+
decision: "decisionId"
|
|
7621
|
+
};
|
|
7307
7622
|
const app = stringOpt(parsed.options.app);
|
|
7308
7623
|
if (app) q.set("app", app);
|
|
7309
7624
|
for (const [flag, param] of Object.entries(filters)) {
|
|
@@ -7316,7 +7631,7 @@ async function pmList(ctx, entity, parsed) {
|
|
|
7316
7631
|
}
|
|
7317
7632
|
const qs = q.toString();
|
|
7318
7633
|
const page = await pmRequest(ctx, "GET", `/${entity}${qs ? `?${qs}` : ""}`);
|
|
7319
|
-
|
|
7634
|
+
emit2(ctx, page, () => {
|
|
7320
7635
|
ctx.out.log(`${entity} \u2014 ${page.records.length} of ${page.total}`);
|
|
7321
7636
|
ctx.out.log("id state app title");
|
|
7322
7637
|
for (const r of page.records) printRecord(ctx, entity, r);
|
|
@@ -7325,25 +7640,82 @@ async function pmList(ctx, entity, parsed) {
|
|
|
7325
7640
|
async function pmAdd(ctx, entity, parsed) {
|
|
7326
7641
|
const appId = stringOpt(parsed.options.app);
|
|
7327
7642
|
if (!appId) throw new Error("pm add needs --app <appId>");
|
|
7328
|
-
const input =
|
|
7643
|
+
const input = collectEntityFields(entity, parsed, false);
|
|
7329
7644
|
if (!input.title) throw new Error("pm add needs --title <title>");
|
|
7645
|
+
if (entity === "bug" && !input.description)
|
|
7646
|
+
throw new Error("pm bug add needs --description <context> (or --body <context>)");
|
|
7330
7647
|
const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
|
|
7331
|
-
|
|
7648
|
+
emit2(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
|
|
7332
7649
|
}
|
|
7333
7650
|
async function pmGet(ctx, entity, id) {
|
|
7334
7651
|
const { record: record5 } = await pmRequest(ctx, "GET", `/${entity}/${encodeURIComponent(id)}`);
|
|
7335
|
-
|
|
7652
|
+
emit2(ctx, record5, () => printRecord(ctx, entity, record5));
|
|
7336
7653
|
}
|
|
7337
7654
|
async function pmSet(ctx, entity, id, parsed) {
|
|
7338
|
-
const patch2 =
|
|
7655
|
+
const patch2 = collectEntityFields(entity, parsed, true);
|
|
7339
7656
|
if (Object.keys(patch2).length === 0)
|
|
7340
7657
|
throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
|
|
7341
7658
|
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
7342
|
-
|
|
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}`));
|
|
7343
7660
|
}
|
|
7344
|
-
async function pmDone(ctx, entity, id) {
|
|
7345
|
-
const
|
|
7346
|
-
|
|
7661
|
+
async function pmDone(ctx, entity, id, parsed) {
|
|
7662
|
+
const decisionId = stringOpt(parsed.options.decision);
|
|
7663
|
+
if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
|
|
7664
|
+
const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
|
|
7665
|
+
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
7666
|
+
emit2(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
|
|
7667
|
+
}
|
|
7668
|
+
async function allRecords(ctx, entity, appId) {
|
|
7669
|
+
const records = [];
|
|
7670
|
+
for (; ; ) {
|
|
7671
|
+
const q = new URLSearchParams({
|
|
7672
|
+
app: appId,
|
|
7673
|
+
limit: "100",
|
|
7674
|
+
offset: String(records.length)
|
|
7675
|
+
});
|
|
7676
|
+
const page = await pmRequest(ctx, "GET", `/${entity}?${q}`);
|
|
7677
|
+
records.push(...page.records);
|
|
7678
|
+
if (records.length >= page.total || page.records.length === 0) return records;
|
|
7679
|
+
}
|
|
7680
|
+
}
|
|
7681
|
+
async function pmHandoff(ctx, parsed) {
|
|
7682
|
+
const appId = stringOpt(parsed.options.app);
|
|
7683
|
+
if (!appId) throw new Error("pm handoff needs --app <appId>");
|
|
7684
|
+
const [goals, tasks, bugs] = await Promise.all([
|
|
7685
|
+
allRecords(ctx, "goal", appId),
|
|
7686
|
+
allRecords(ctx, "task", appId),
|
|
7687
|
+
allRecords(ctx, "bug", appId)
|
|
7688
|
+
]);
|
|
7689
|
+
const handoff = {
|
|
7690
|
+
appId,
|
|
7691
|
+
unmetGoals: goals.filter((record5) => record5.status !== "met"),
|
|
7692
|
+
activeTasks: tasks.filter((record5) => record5.column !== "done"),
|
|
7693
|
+
openBugs: bugs.filter((record5) => record5.status !== "fixed" && record5.status !== "wontfix")
|
|
7694
|
+
};
|
|
7695
|
+
const result = {
|
|
7696
|
+
...handoff,
|
|
7697
|
+
clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
|
|
7698
|
+
};
|
|
7699
|
+
emit2(ctx, result, () => {
|
|
7700
|
+
if (result.clean) {
|
|
7701
|
+
ctx.out.log(`${appId}: no unresolved PM work`);
|
|
7702
|
+
return;
|
|
7703
|
+
}
|
|
7704
|
+
ctx.out.log(`${appId}: authoritative PM handoff`);
|
|
7705
|
+
for (const [label, records] of [
|
|
7706
|
+
["unmet goals", result.unmetGoals],
|
|
7707
|
+
["active tasks", result.activeTasks],
|
|
7708
|
+
["open bugs", result.openBugs]
|
|
7709
|
+
]) {
|
|
7710
|
+
ctx.out.log(`${label}:`);
|
|
7711
|
+
if (!records.length) ctx.out.log("- (none)");
|
|
7712
|
+
else for (const record5 of records) printRecord(
|
|
7713
|
+
ctx,
|
|
7714
|
+
label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
|
|
7715
|
+
record5
|
|
7716
|
+
);
|
|
7717
|
+
}
|
|
7718
|
+
});
|
|
7347
7719
|
}
|
|
7348
7720
|
async function pmComment(ctx, entity, id, parsed) {
|
|
7349
7721
|
const body = stringOpt(parsed.options.body);
|
|
@@ -7357,7 +7729,7 @@ async function pmComments(ctx, entity, id) {
|
|
|
7357
7729
|
"GET",
|
|
7358
7730
|
`/${entity}/${encodeURIComponent(id)}/comments`
|
|
7359
7731
|
);
|
|
7360
|
-
|
|
7732
|
+
emit2(ctx, messages, () => {
|
|
7361
7733
|
if (messages.length === 0) ctx.out.log("(no comments)");
|
|
7362
7734
|
else for (const m of messages) ctx.out.log(`[${m.authorId ?? "?"}] ${m.body ?? ""}`);
|
|
7363
7735
|
});
|
|
@@ -7376,7 +7748,7 @@ var ALIASES = {
|
|
|
7376
7748
|
decision: "decision",
|
|
7377
7749
|
bug: "bug"
|
|
7378
7750
|
};
|
|
7379
|
-
var
|
|
7751
|
+
var ALLOWED2 = [
|
|
7380
7752
|
"config",
|
|
7381
7753
|
"token",
|
|
7382
7754
|
"email",
|
|
@@ -7395,15 +7767,16 @@ var ALLOWED = [
|
|
|
7395
7767
|
"target",
|
|
7396
7768
|
"description",
|
|
7397
7769
|
"desc",
|
|
7770
|
+
"decision",
|
|
7398
7771
|
"limit",
|
|
7399
7772
|
"offset",
|
|
7400
7773
|
"q"
|
|
7401
7774
|
];
|
|
7402
|
-
function
|
|
7775
|
+
function requireId2(id, action) {
|
|
7403
7776
|
if (!id) throw new Error(`"pm ... ${action}" needs an item id`);
|
|
7404
7777
|
return id;
|
|
7405
7778
|
}
|
|
7406
|
-
async function
|
|
7779
|
+
async function buildContext2(parsed, deps) {
|
|
7407
7780
|
const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
|
|
7408
7781
|
const doFetch = deps.fetch ?? fetch;
|
|
7409
7782
|
const out = deps.stdout ?? console;
|
|
@@ -7417,11 +7790,15 @@ async function buildContext(parsed, deps) {
|
|
|
7417
7790
|
}
|
|
7418
7791
|
async function pmCommand(parsed, deps = {}) {
|
|
7419
7792
|
const word = parsed.positionals[1] ?? "";
|
|
7793
|
+
if (word === "handoff") {
|
|
7794
|
+
assertArgs(parsed, ALLOWED2, 2);
|
|
7795
|
+
return pmHandoff(await buildContext2(parsed, deps), parsed);
|
|
7796
|
+
}
|
|
7420
7797
|
const entity = ALIASES[word];
|
|
7421
7798
|
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
7422
7799
|
const action = parsed.positionals[2] ?? "list";
|
|
7423
|
-
assertArgs(parsed,
|
|
7424
|
-
const ctx = await
|
|
7800
|
+
assertArgs(parsed, ALLOWED2, 4);
|
|
7801
|
+
const ctx = await buildContext2(parsed, deps);
|
|
7425
7802
|
const id = parsed.positionals[3];
|
|
7426
7803
|
switch (action) {
|
|
7427
7804
|
case "list":
|
|
@@ -7430,21 +7807,21 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
7430
7807
|
case "create":
|
|
7431
7808
|
return pmAdd(ctx, entity, parsed);
|
|
7432
7809
|
case "get":
|
|
7433
|
-
return pmGet(ctx, entity,
|
|
7810
|
+
return pmGet(ctx, entity, requireId2(id, action));
|
|
7434
7811
|
case "set":
|
|
7435
7812
|
case "update":
|
|
7436
7813
|
case "status":
|
|
7437
7814
|
case "move":
|
|
7438
|
-
return pmSet(ctx, entity,
|
|
7815
|
+
return pmSet(ctx, entity, requireId2(id, action), parsed);
|
|
7439
7816
|
case "done":
|
|
7440
|
-
return pmDone(ctx, entity,
|
|
7817
|
+
return pmDone(ctx, entity, requireId2(id, action), parsed);
|
|
7441
7818
|
case "comment":
|
|
7442
|
-
return pmComment(ctx, entity,
|
|
7819
|
+
return pmComment(ctx, entity, requireId2(id, action), parsed);
|
|
7443
7820
|
case "comments":
|
|
7444
|
-
return pmComments(ctx, entity,
|
|
7821
|
+
return pmComments(ctx, entity, requireId2(id, action));
|
|
7445
7822
|
case "rm":
|
|
7446
7823
|
case "delete":
|
|
7447
|
-
return pmRemove(ctx, entity,
|
|
7824
|
+
return pmRemove(ctx, entity, requireId2(id, action));
|
|
7448
7825
|
default:
|
|
7449
7826
|
throw new Error(`unknown pm action "${action}". Try list|add|get|set|done|comment|comments|rm.`);
|
|
7450
7827
|
}
|
|
@@ -7910,10 +8287,25 @@ var COMMAND_SURFACE = {
|
|
|
7910
8287
|
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
7911
8288
|
capabilities: {},
|
|
7912
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
|
+
},
|
|
7913
8302
|
doctor: {},
|
|
7914
8303
|
help: {},
|
|
7915
8304
|
init: {},
|
|
7916
|
-
pm:
|
|
8305
|
+
pm: {
|
|
8306
|
+
...PM_ENTITIES,
|
|
8307
|
+
handoff: {}
|
|
8308
|
+
},
|
|
7917
8309
|
provision: {},
|
|
7918
8310
|
runbook: {
|
|
7919
8311
|
ask: {},
|
|
@@ -8503,7 +8895,7 @@ async function assessImpact(ctx, surfaces, all, limit) {
|
|
|
8503
8895
|
return out;
|
|
8504
8896
|
}
|
|
8505
8897
|
var editHint = (slug, appId) => `odla-ai runbook edit ${slug}${appId === PLATFORM_SCOPE ? "" : ` --app ${appId}`} --note "<what changed>"`;
|
|
8506
|
-
function
|
|
8898
|
+
function report3(ctx, impacts) {
|
|
8507
8899
|
const covered = impacts.filter((i) => i.runbooks.length);
|
|
8508
8900
|
ctx.out.log(
|
|
8509
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.`
|
|
@@ -8541,7 +8933,7 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
8541
8933
|
}
|
|
8542
8934
|
const impacts = await assessImpact(ctx, surfaces, options.all, options.limit ?? 4);
|
|
8543
8935
|
if (ctx.json) return ctx.out.log(JSON.stringify({ base: options.base, impacts }, null, 2));
|
|
8544
|
-
|
|
8936
|
+
report3(ctx, impacts);
|
|
8545
8937
|
}
|
|
8546
8938
|
|
|
8547
8939
|
// src/runbook-lint.ts
|
|
@@ -8774,7 +9166,7 @@ async function whoamiCommand(parsed, deps = {}) {
|
|
|
8774
9166
|
}
|
|
8775
9167
|
|
|
8776
9168
|
// src/runbook-command.ts
|
|
8777
|
-
var
|
|
9169
|
+
var ALLOWED3 = [
|
|
8778
9170
|
"config",
|
|
8779
9171
|
"token",
|
|
8780
9172
|
"email",
|
|
@@ -8814,7 +9206,7 @@ async function loadOrDefaultConfig(configPath, action, appId) {
|
|
|
8814
9206
|
}
|
|
8815
9207
|
return loadProjectConfig(configPath);
|
|
8816
9208
|
}
|
|
8817
|
-
async function
|
|
9209
|
+
async function buildContext3(parsed, deps, action) {
|
|
8818
9210
|
const configPath = stringOpt(parsed.options.config) ?? "odla.config.mjs";
|
|
8819
9211
|
const cfg = await loadOrDefaultConfig(configPath, action, stringOpt(parsed.options.app));
|
|
8820
9212
|
const doFetch = deps.fetch ?? fetch;
|
|
@@ -8870,8 +9262,8 @@ async function buildContext2(parsed, deps, action) {
|
|
|
8870
9262
|
}
|
|
8871
9263
|
async function runbookCommand(parsed, deps = {}) {
|
|
8872
9264
|
const action = parsed.positionals[1] ?? "list";
|
|
8873
|
-
assertArgs(parsed,
|
|
8874
|
-
const ctx = await
|
|
9265
|
+
assertArgs(parsed, ALLOWED3, action === "ask" || action === "search" ? 64 : 4);
|
|
9266
|
+
const ctx = await buildContext3(parsed, deps, action);
|
|
8875
9267
|
const slug = parsed.positionals[2];
|
|
8876
9268
|
switch (action) {
|
|
8877
9269
|
case "list":
|
|
@@ -9062,31 +9454,31 @@ function printHostedJob(out, job, platform, appId) {
|
|
|
9062
9454
|
url.searchParams.set("job", job.jobId);
|
|
9063
9455
|
out.log(` Studio: ${url.toString()}`);
|
|
9064
9456
|
}
|
|
9065
|
-
function printHostedReport(out,
|
|
9066
|
-
out.log(`security report ${
|
|
9067
|
-
out.log(` coverage: ${
|
|
9068
|
-
out.log(` findings: confirmed=${
|
|
9069
|
-
out.log(` discovery: ${
|
|
9070
|
-
out.log(` validation: ${
|
|
9071
|
-
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) {
|
|
9072
9464
|
const location = finding.locations[0];
|
|
9073
9465
|
out.log(` [${finding.severity}] ${finding.title}${location ? ` (${location.path}:${location.line})` : ""} \xB7 ${finding.disposition}`);
|
|
9074
9466
|
}
|
|
9075
|
-
for (const limitation of
|
|
9467
|
+
for (const limitation of report4.limitations) out.log(` limitation: ${limitation}`);
|
|
9076
9468
|
}
|
|
9077
|
-
function enforceHostedReportGate(
|
|
9469
|
+
function enforceHostedReportGate(report4, parsed, out, emitSuccess) {
|
|
9078
9470
|
const failOn = hostedSeverity(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
9079
9471
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
9080
9472
|
const failOnCandidates = candidateValue === false ? void 0 : hostedSeverity(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
9081
9473
|
const atOrAbove = (severity, threshold) => HOSTED_SEVERITIES.indexOf(severity) >= HOSTED_SEVERITIES.indexOf(threshold);
|
|
9082
|
-
const confirmed =
|
|
9083
|
-
const leads = failOnCandidates ?
|
|
9084
|
-
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;
|
|
9085
9477
|
if (confirmed.length || leads.length || incomplete) {
|
|
9086
|
-
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}` : ""}`);
|
|
9087
9479
|
}
|
|
9088
9480
|
if (emitSuccess) {
|
|
9089
|
-
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.`);
|
|
9090
9482
|
}
|
|
9091
9483
|
}
|
|
9092
9484
|
function printHostedSecurityPlanRoute(out, label, route2) {
|
|
@@ -9169,17 +9561,17 @@ async function runHostedSecurity(options) {
|
|
|
9169
9561
|
allowNetwork: false
|
|
9170
9562
|
}
|
|
9171
9563
|
});
|
|
9172
|
-
const
|
|
9173
|
-
await (0, import_node3.writeSecurityArtifacts)(output,
|
|
9174
|
-
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);
|
|
9175
9567
|
await hosted.complete({
|
|
9176
9568
|
reportDigest,
|
|
9177
|
-
coverageStatus:
|
|
9178
|
-
confirmed:
|
|
9179
|
-
candidates:
|
|
9569
|
+
coverageStatus: report4.coverageStatus,
|
|
9570
|
+
confirmed: report4.metrics.confirmed,
|
|
9571
|
+
candidates: report4.metrics.candidates
|
|
9180
9572
|
}, { signal: options.signal });
|
|
9181
|
-
printSummary(options.stdout ?? console, appId, env, hosted.run,
|
|
9182
|
-
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 });
|
|
9183
9575
|
}
|
|
9184
9576
|
function selectEnv(requested, declared, configPath, rootDir) {
|
|
9185
9577
|
const env = requested ?? (declared.includes("dev") ? "dev" : declared[0]);
|
|
@@ -9189,11 +9581,11 @@ function selectEnv(requested, declared, configPath, rootDir) {
|
|
|
9189
9581
|
}
|
|
9190
9582
|
return env;
|
|
9191
9583
|
}
|
|
9192
|
-
async function injectedToken(options,
|
|
9193
|
-
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 }));
|
|
9194
9586
|
if (typeof value !== "string" || value.length < 8 || value.length > 8192 || /\s|[\u0000-\u001f\u007f]/.test(value)) {
|
|
9195
9587
|
throw new Error(
|
|
9196
|
-
|
|
9588
|
+
request2.selfAudit ? "Self-audit requires an injected, scoped platform security token" : "Hosted security requires an injected app developer token or getToken callback"
|
|
9197
9589
|
);
|
|
9198
9590
|
}
|
|
9199
9591
|
return value;
|
|
@@ -9205,14 +9597,14 @@ function profileFor(name, maxHuntTasks) {
|
|
|
9205
9597
|
if (!Number.isSafeInteger(maxHuntTasks) || maxHuntTasks < 1) throw new Error("maxHuntTasks must be a positive integer");
|
|
9206
9598
|
return { ...profile, maxHuntTasks };
|
|
9207
9599
|
}
|
|
9208
|
-
function printSummary(out, appId, env, run,
|
|
9209
|
-
const complete =
|
|
9600
|
+
function printSummary(out, appId, env, run, report4, output) {
|
|
9601
|
+
const complete = report4.coverage.filter((cell) => cell.state === "complete").length;
|
|
9210
9602
|
out.log(`security: ${appId}/${env} run=${run.runId} profile=${run.profileVersion}`);
|
|
9211
9603
|
out.log(` discovery: ${run.discovery.identity.provider}/${run.discovery.identity.model}`);
|
|
9212
9604
|
out.log(` validation: ${run.validation.identity.provider}/${run.validation.identity.model}`);
|
|
9213
|
-
out.log(` coverage: ${
|
|
9214
|
-
if (
|
|
9215
|
-
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}`);
|
|
9216
9608
|
out.log(` report: ${(0, import_node_path15.resolve)(output, "REPORT.md")}`);
|
|
9217
9609
|
}
|
|
9218
9610
|
function formatBudget(usage) {
|
|
@@ -9459,13 +9851,13 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
|
|
|
9459
9851
|
}
|
|
9460
9852
|
throw new Error(`hosted security job ${result.jobId} ended ${result.status}${result.errorCode ? `: ${result.errorCode}` : ""}`);
|
|
9461
9853
|
}
|
|
9462
|
-
const
|
|
9854
|
+
const report4 = await getHostedSecurityReport({ ...context, jobId: result.jobId });
|
|
9463
9855
|
if (parsed.options.json === true) {
|
|
9464
|
-
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));
|
|
9465
9857
|
} else {
|
|
9466
|
-
printHostedReport(context.stdout,
|
|
9858
|
+
printHostedReport(context.stdout, report4);
|
|
9467
9859
|
}
|
|
9468
|
-
enforceHostedReportGate(
|
|
9860
|
+
enforceHostedReportGate(report4, parsed, context.stdout, parsed.options.json !== true);
|
|
9469
9861
|
}
|
|
9470
9862
|
async function runLocalSecurityCommand(parsed, dependencies) {
|
|
9471
9863
|
if (parsed.options.source === true) {
|
|
@@ -9506,11 +9898,11 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9506
9898
|
sourceDisclosureAck: parsed.options["ack-redacted-source"] === true ? "redacted" : void 0,
|
|
9507
9899
|
fetch: doFetch,
|
|
9508
9900
|
stdout: out,
|
|
9509
|
-
getToken: async (
|
|
9510
|
-
if (
|
|
9901
|
+
getToken: async (request2) => {
|
|
9902
|
+
if (request2.scope === "platform:security:self") {
|
|
9511
9903
|
return getScopedPlatformToken({
|
|
9512
|
-
platform:
|
|
9513
|
-
scope:
|
|
9904
|
+
platform: request2.platform,
|
|
9905
|
+
scope: request2.scope,
|
|
9514
9906
|
email: stringOpt(parsed.options.email),
|
|
9515
9907
|
open,
|
|
9516
9908
|
fetch: doFetch,
|
|
@@ -9519,7 +9911,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9519
9911
|
});
|
|
9520
9912
|
}
|
|
9521
9913
|
const cfg = await loadProjectConfig(configPath);
|
|
9522
|
-
if (platformAudience(cfg.platformUrl) !== platformAudience(
|
|
9914
|
+
if (platformAudience(cfg.platformUrl) !== platformAudience(request2.platform)) {
|
|
9523
9915
|
throw new Error("--platform cannot reuse a project developer token from another platform; update odla.config.mjs and authenticate there");
|
|
9524
9916
|
}
|
|
9525
9917
|
return getDeveloperToken(
|
|
@@ -9532,13 +9924,13 @@ async function runLocalSecurityCommand(parsed, dependencies) {
|
|
|
9532
9924
|
});
|
|
9533
9925
|
enforceLocalGate(result.report, parsed);
|
|
9534
9926
|
}
|
|
9535
|
-
function enforceLocalGate(
|
|
9927
|
+
function enforceLocalGate(report4, parsed) {
|
|
9536
9928
|
const failOn = severityOpt(stringOpt(parsed.options["fail-on"]) ?? "high", "--fail-on");
|
|
9537
9929
|
const candidateValue = parsed.options["fail-on-candidates"];
|
|
9538
9930
|
const failOnCandidates = candidateValue === false ? void 0 : severityOpt(stringOpt(candidateValue) ?? "critical", "--fail-on-candidates");
|
|
9539
|
-
const confirmed = (0, import_security2.findingsAtOrAbove)(
|
|
9540
|
-
const leads = failOnCandidates ? (0, import_security2.findingsAtOrAbove)(
|
|
9541
|
-
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;
|
|
9542
9934
|
if (confirmed.length || leads.length || incomplete) {
|
|
9543
9935
|
throw new Error(`hosted security gate failed: ${confirmed.length} confirmed >= ${failOn}; ${leads.length} leads >= ${failOnCandidates ?? "disabled"}${incomplete ? "; coverage incomplete" : ""}`);
|
|
9544
9936
|
}
|
|
@@ -9577,9 +9969,9 @@ async function securityCommand(parsed, dependencies) {
|
|
|
9577
9969
|
assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
|
|
9578
9970
|
const jobId = requiredSecurityPositional(parsed, 2, "job id");
|
|
9579
9971
|
const context = await hostedSecurityContext(parsed, dependencies);
|
|
9580
|
-
const
|
|
9581
|
-
if (parsed.options.json === true) context.stdout.log(JSON.stringify(
|
|
9582
|
-
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);
|
|
9583
9975
|
return;
|
|
9584
9976
|
}
|
|
9585
9977
|
if (sub !== "run") {
|
|
@@ -9663,7 +10055,8 @@ async function securityStatus(parsed, dependencies) {
|
|
|
9663
10055
|
|
|
9664
10056
|
// src/cli.ts
|
|
9665
10057
|
function exitCodeFor(err) {
|
|
9666
|
-
|
|
10058
|
+
const code = err?.code;
|
|
10059
|
+
return code === "handshake_pending" || code === "watch_timeout" ? 75 : 1;
|
|
9667
10060
|
}
|
|
9668
10061
|
async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
9669
10062
|
const parsed = parseArgv(argv);
|
|
@@ -9711,6 +10104,10 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
|
|
|
9711
10104
|
await pmCommand(parsed, dependencies);
|
|
9712
10105
|
return;
|
|
9713
10106
|
}
|
|
10107
|
+
if (command === "discuss") {
|
|
10108
|
+
await discussCommand(parsed, dependencies);
|
|
10109
|
+
return;
|
|
10110
|
+
}
|
|
9714
10111
|
if (command === "provision") {
|
|
9715
10112
|
await provisionCommand(parsed, dependencies);
|
|
9716
10113
|
return;
|