@rosthq/cli 0.7.154 → 0.7.155
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/forge-workspace.d.ts +30 -6
- package/dist/forge-workspace.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1285 -340
- package/dist/index.js.map +4 -4
- package/dist/runner-login.d.ts +29 -0
- package/dist/runner-login.d.ts.map +1 -0
- package/dist/runner-sandbox.d.ts +6 -1
- package/dist/runner-sandbox.d.ts.map +1 -1
- package/dist/runner-serve.d.ts +8 -2
- package/dist/runner-serve.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -472,13 +472,13 @@ function __disposeResources(env) {
|
|
|
472
472
|
}
|
|
473
473
|
return next();
|
|
474
474
|
}
|
|
475
|
-
function __rewriteRelativeImportExtension(
|
|
476
|
-
if (typeof
|
|
477
|
-
return
|
|
475
|
+
function __rewriteRelativeImportExtension(path12, preserveJsx) {
|
|
476
|
+
if (typeof path12 === "string" && /^\.\.?\//.test(path12)) {
|
|
477
|
+
return path12.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
|
|
478
478
|
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
|
|
479
479
|
});
|
|
480
480
|
}
|
|
481
|
-
return
|
|
481
|
+
return path12;
|
|
482
482
|
}
|
|
483
483
|
var extendStatics, __assign, __createBinding, __setModuleDefault, ownKeys, _SuppressedError, tslib_es6_default;
|
|
484
484
|
var init_tslib_es6 = __esm({
|
|
@@ -14337,10 +14337,10 @@ function mergeDefs(...defs) {
|
|
|
14337
14337
|
function cloneDef(schema) {
|
|
14338
14338
|
return mergeDefs(schema._zod.def);
|
|
14339
14339
|
}
|
|
14340
|
-
function getElementAtPath(obj,
|
|
14341
|
-
if (!
|
|
14340
|
+
function getElementAtPath(obj, path12) {
|
|
14341
|
+
if (!path12)
|
|
14342
14342
|
return obj;
|
|
14343
|
-
return
|
|
14343
|
+
return path12.reduce((acc, key) => acc?.[key], obj);
|
|
14344
14344
|
}
|
|
14345
14345
|
function promiseAllObject(promisesObj) {
|
|
14346
14346
|
const keys = Object.keys(promisesObj);
|
|
@@ -14749,11 +14749,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
14749
14749
|
}
|
|
14750
14750
|
return false;
|
|
14751
14751
|
}
|
|
14752
|
-
function prefixIssues(
|
|
14752
|
+
function prefixIssues(path12, issues) {
|
|
14753
14753
|
return issues.map((iss) => {
|
|
14754
14754
|
var _a3;
|
|
14755
14755
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
14756
|
-
iss.path.unshift(
|
|
14756
|
+
iss.path.unshift(path12);
|
|
14757
14757
|
return iss;
|
|
14758
14758
|
});
|
|
14759
14759
|
}
|
|
@@ -14900,16 +14900,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
|
|
|
14900
14900
|
}
|
|
14901
14901
|
function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
14902
14902
|
const fieldErrors = { _errors: [] };
|
|
14903
|
-
const processError = (error52,
|
|
14903
|
+
const processError = (error52, path12 = []) => {
|
|
14904
14904
|
for (const issue2 of error52.issues) {
|
|
14905
14905
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
14906
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
14906
|
+
issue2.errors.map((issues) => processError({ issues }, [...path12, ...issue2.path]));
|
|
14907
14907
|
} else if (issue2.code === "invalid_key") {
|
|
14908
|
-
processError({ issues: issue2.issues }, [...
|
|
14908
|
+
processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
|
|
14909
14909
|
} else if (issue2.code === "invalid_element") {
|
|
14910
|
-
processError({ issues: issue2.issues }, [...
|
|
14910
|
+
processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
|
|
14911
14911
|
} else {
|
|
14912
|
-
const fullpath = [...
|
|
14912
|
+
const fullpath = [...path12, ...issue2.path];
|
|
14913
14913
|
if (fullpath.length === 0) {
|
|
14914
14914
|
fieldErrors._errors.push(mapper(issue2));
|
|
14915
14915
|
} else {
|
|
@@ -14936,17 +14936,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
|
14936
14936
|
}
|
|
14937
14937
|
function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
14938
14938
|
const result = { errors: [] };
|
|
14939
|
-
const processError = (error52,
|
|
14939
|
+
const processError = (error52, path12 = []) => {
|
|
14940
14940
|
var _a3, _b;
|
|
14941
14941
|
for (const issue2 of error52.issues) {
|
|
14942
14942
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
14943
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
14943
|
+
issue2.errors.map((issues) => processError({ issues }, [...path12, ...issue2.path]));
|
|
14944
14944
|
} else if (issue2.code === "invalid_key") {
|
|
14945
|
-
processError({ issues: issue2.issues }, [...
|
|
14945
|
+
processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
|
|
14946
14946
|
} else if (issue2.code === "invalid_element") {
|
|
14947
|
-
processError({ issues: issue2.issues }, [...
|
|
14947
|
+
processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
|
|
14948
14948
|
} else {
|
|
14949
|
-
const fullpath = [...
|
|
14949
|
+
const fullpath = [...path12, ...issue2.path];
|
|
14950
14950
|
if (fullpath.length === 0) {
|
|
14951
14951
|
result.errors.push(mapper(issue2));
|
|
14952
14952
|
continue;
|
|
@@ -14978,8 +14978,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
|
14978
14978
|
}
|
|
14979
14979
|
function toDotPath(_path) {
|
|
14980
14980
|
const segs = [];
|
|
14981
|
-
const
|
|
14982
|
-
for (const seg of
|
|
14981
|
+
const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
14982
|
+
for (const seg of path12) {
|
|
14983
14983
|
if (typeof seg === "number")
|
|
14984
14984
|
segs.push(`[${seg}]`);
|
|
14985
14985
|
else if (typeof seg === "symbol")
|
|
@@ -27671,13 +27671,13 @@ function resolveRef(ref, ctx) {
|
|
|
27671
27671
|
if (!ref.startsWith("#")) {
|
|
27672
27672
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
27673
27673
|
}
|
|
27674
|
-
const
|
|
27675
|
-
if (
|
|
27674
|
+
const path12 = ref.slice(1).split("/").filter(Boolean);
|
|
27675
|
+
if (path12.length === 0) {
|
|
27676
27676
|
return ctx.rootSchema;
|
|
27677
27677
|
}
|
|
27678
27678
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
27679
|
-
if (
|
|
27680
|
-
const key =
|
|
27679
|
+
if (path12[0] === defsKey) {
|
|
27680
|
+
const key = path12[1];
|
|
27681
27681
|
if (!key || !ctx.defs[key]) {
|
|
27682
27682
|
throw new Error(`Reference not found: ${ref}`);
|
|
27683
27683
|
}
|
|
@@ -30606,7 +30606,7 @@ var forbiddenImportKeys = /* @__PURE__ */ new Set([
|
|
|
30606
30606
|
"integration_id",
|
|
30607
30607
|
"integrationId"
|
|
30608
30608
|
]);
|
|
30609
|
-
function rejectForbiddenImportShape(value, ctx,
|
|
30609
|
+
function rejectForbiddenImportShape(value, ctx, path12 = []) {
|
|
30610
30610
|
if (value === null || value === void 0) {
|
|
30611
30611
|
return;
|
|
30612
30612
|
}
|
|
@@ -30614,21 +30614,21 @@ function rejectForbiddenImportShape(value, ctx, path11 = []) {
|
|
|
30614
30614
|
if (hasSecretShapedValue(value)) {
|
|
30615
30615
|
ctx.addIssue({
|
|
30616
30616
|
code: external_exports.ZodIssueCode.custom,
|
|
30617
|
-
path:
|
|
30617
|
+
path: path12,
|
|
30618
30618
|
message: "Definition imports cannot contain secret-shaped values. Provide credentials later through vault-backed connection flows."
|
|
30619
30619
|
});
|
|
30620
30620
|
}
|
|
30621
30621
|
return;
|
|
30622
30622
|
}
|
|
30623
30623
|
if (Array.isArray(value)) {
|
|
30624
|
-
value.forEach((item, index) => rejectForbiddenImportShape(item, ctx, [...
|
|
30624
|
+
value.forEach((item, index) => rejectForbiddenImportShape(item, ctx, [...path12, index]));
|
|
30625
30625
|
return;
|
|
30626
30626
|
}
|
|
30627
30627
|
if (typeof value !== "object") {
|
|
30628
30628
|
return;
|
|
30629
30629
|
}
|
|
30630
30630
|
for (const [key, nested] of Object.entries(value)) {
|
|
30631
|
-
const nestedPath = [...
|
|
30631
|
+
const nestedPath = [...path12, key];
|
|
30632
30632
|
if (forbiddenImportKeys.has(key)) {
|
|
30633
30633
|
ctx.addIssue({
|
|
30634
30634
|
code: external_exports.ZodIssueCode.custom,
|
|
@@ -34622,7 +34622,7 @@ var preflightArtifactDispositionSchema = external_exports.object({
|
|
|
34622
34622
|
// on this envelope.
|
|
34623
34623
|
transformations: external_exports.array(preflightTransformationSchema)
|
|
34624
34624
|
}).strict().superRefine((data, ctx) => {
|
|
34625
|
-
const add = (
|
|
34625
|
+
const add = (path12, message) => ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: path12, message });
|
|
34626
34626
|
const reasonIsBlank = data.reason !== null && data.reason.trim().length === 0;
|
|
34627
34627
|
if (reasonIsBlank) add(["reason"], "reason must be nonblank when supplied");
|
|
34628
34628
|
if (data.presence === "missing") {
|
|
@@ -35661,14 +35661,14 @@ var COMPLETE_SEAT_PERMISSION_ARGS_MAX_NODES = 5e3;
|
|
|
35661
35661
|
function jsonByteLength(value) {
|
|
35662
35662
|
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
35663
35663
|
}
|
|
35664
|
-
function boundCharterList(value,
|
|
35664
|
+
function boundCharterList(value, path12, ctx) {
|
|
35665
35665
|
if (value.length > COMPLETE_SEAT_CHARTER_MAX_ITEMS_PER_LIST) {
|
|
35666
35666
|
ctx.addIssue({
|
|
35667
35667
|
code: external_exports.ZodIssueCode.too_big,
|
|
35668
35668
|
maximum: COMPLETE_SEAT_CHARTER_MAX_ITEMS_PER_LIST,
|
|
35669
35669
|
origin: "array",
|
|
35670
35670
|
inclusive: true,
|
|
35671
|
-
path:
|
|
35671
|
+
path: path12,
|
|
35672
35672
|
message: `Complete-Seat Charter lists are limited to ${COMPLETE_SEAT_CHARTER_MAX_ITEMS_PER_LIST} entries.`
|
|
35673
35673
|
});
|
|
35674
35674
|
}
|
|
@@ -35685,7 +35685,7 @@ function addCompleteSeatCharterBounds(charter, ctx) {
|
|
|
35685
35685
|
boundCharterList(charter.unanswered_boundaries, ["unanswered_boundaries"], ctx);
|
|
35686
35686
|
}
|
|
35687
35687
|
let argumentNodes = 0;
|
|
35688
|
-
const visitArgument = (value, depth,
|
|
35688
|
+
const visitArgument = (value, depth, path12) => {
|
|
35689
35689
|
argumentNodes += 1;
|
|
35690
35690
|
if (argumentNodes > COMPLETE_SEAT_PERMISSION_ARGS_MAX_NODES) {
|
|
35691
35691
|
return;
|
|
@@ -35693,15 +35693,15 @@ function addCompleteSeatCharterBounds(charter, ctx) {
|
|
|
35693
35693
|
if (depth > COMPLETE_SEAT_PERMISSION_ARGS_MAX_DEPTH) {
|
|
35694
35694
|
ctx.addIssue({
|
|
35695
35695
|
code: external_exports.ZodIssueCode.custom,
|
|
35696
|
-
path:
|
|
35696
|
+
path: path12,
|
|
35697
35697
|
message: `Complete-Seat permission arguments are limited to ${COMPLETE_SEAT_PERMISSION_ARGS_MAX_DEPTH} nested levels.`
|
|
35698
35698
|
});
|
|
35699
35699
|
return;
|
|
35700
35700
|
}
|
|
35701
35701
|
if (Array.isArray(value)) {
|
|
35702
|
-
value.forEach((entry, index) => visitArgument(entry, depth + 1, [...
|
|
35702
|
+
value.forEach((entry, index) => visitArgument(entry, depth + 1, [...path12, index]));
|
|
35703
35703
|
} else if (value && typeof value === "object") {
|
|
35704
|
-
Object.entries(value).forEach(([key, entry]) => visitArgument(entry, depth + 1, [...
|
|
35704
|
+
Object.entries(value).forEach(([key, entry]) => visitArgument(entry, depth + 1, [...path12, key]));
|
|
35705
35705
|
}
|
|
35706
35706
|
};
|
|
35707
35707
|
charter.permission_manifest.forEach((permission, index) => {
|
|
@@ -35862,9 +35862,9 @@ var createCompleteSeatRequestSchema = external_exports.object({
|
|
|
35862
35862
|
}
|
|
35863
35863
|
const useKeys = /* @__PURE__ */ new Set();
|
|
35864
35864
|
for (const [index, use] of value.seat.source_uses.entries()) {
|
|
35865
|
-
const
|
|
35866
|
-
if (!sourceKeys.has(use.source_key)) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path:
|
|
35867
|
-
if (useKeys.has(use.source_key)) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path:
|
|
35865
|
+
const path12 = ["seat", "source_uses", index, "source_key"];
|
|
35866
|
+
if (!sourceKeys.has(use.source_key)) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: path12, message: "Source use must resolve to the request source registry." });
|
|
35867
|
+
if (useKeys.has(use.source_key)) ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: path12, message: "Source use must not be duplicated." });
|
|
35868
35868
|
useKeys.add(use.source_key);
|
|
35869
35869
|
}
|
|
35870
35870
|
if (value.seat.human_staffing.kind === "planned" && value.seat.human_staffing.source_ref) {
|
|
@@ -35877,8 +35877,8 @@ var createCompleteSeatRequestSchema = external_exports.object({
|
|
|
35877
35877
|
...value.seat.parent ? [{ ref: value.seat.parent, path: ["seat", "parent"] }] : [],
|
|
35878
35878
|
...value.seat.agent ? [{ ref: value.seat.agent.steward, path: ["seat", "agent", "steward"] }] : []
|
|
35879
35879
|
];
|
|
35880
|
-
refs.forEach(({ ref, path:
|
|
35881
|
-
if (ref.kind !== "existing") ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path:
|
|
35880
|
+
refs.forEach(({ ref, path: path12 }) => {
|
|
35881
|
+
if (ref.kind !== "existing") ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: path12, message: "Direct complete-seat requests require existing Seat references." });
|
|
35882
35882
|
});
|
|
35883
35883
|
}).transform((value) => ({
|
|
35884
35884
|
...value,
|
|
@@ -36188,26 +36188,26 @@ var onboardingSourceIngestOutputSchema = external_exports.object({
|
|
|
36188
36188
|
}).strict();
|
|
36189
36189
|
var prohibitedUrlOrPath = /(?:[a-z][a-z0-9+.-]*:\/\/|(?:^|\s)(?:\.{1,2}[\\/]\S+|\/(?:Users|Volumes|Applications|System|Library|private|var|tmp|home|etc)\/|[A-Za-z]:\\))/i;
|
|
36190
36190
|
var directContactOrPaymentIdentifier = /(?:[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}|\+?[0-9][0-9 .()/-]{7,}[0-9]|(?:\b[0-9][ -]*?){13,19}\b)/i;
|
|
36191
|
-
function isIndexedPath(
|
|
36192
|
-
return
|
|
36191
|
+
function isIndexedPath(path12, family, field4) {
|
|
36192
|
+
return path12.length === 3 && path12[0] === family && typeof path12[1] === "number" && path12[2] === field4;
|
|
36193
36193
|
}
|
|
36194
|
-
function isSchemaProvenMachineString(
|
|
36195
|
-
if (
|
|
36194
|
+
function isSchemaProvenMachineString(path12) {
|
|
36195
|
+
if (path12.length === 1 && path12[0] === "managed_inference_hard_cap_usd") {
|
|
36196
36196
|
return true;
|
|
36197
36197
|
}
|
|
36198
|
-
if (isIndexedPath(
|
|
36198
|
+
if (isIndexedPath(path12, "sources", "source_upload_id") || isIndexedPath(path12, "sources", "sha256") || isIndexedPath(path12, "reconciliation", "source_sha256") || isIndexedPath(path12, "reconciliation", "normalized_identity_digest") || isIndexedPath(path12, "signal_readings", "period_start") || isIndexedPath(path12, "signal_readings", "value") || isIndexedPath(path12, "tasks", "due_on")) {
|
|
36199
36199
|
return true;
|
|
36200
36200
|
}
|
|
36201
|
-
if (
|
|
36201
|
+
if (path12.length === 2 && path12[0] === "cycle" && (path12[1] === "starts_on" || path12[1] === "ends_on")) {
|
|
36202
36202
|
return true;
|
|
36203
36203
|
}
|
|
36204
|
-
if (
|
|
36204
|
+
if (path12.length >= 3 && path12.at(-3) === "citations" && typeof path12.at(-2) === "number" && path12.at(-1) === "document_id") {
|
|
36205
36205
|
return true;
|
|
36206
36206
|
}
|
|
36207
|
-
if (
|
|
36207
|
+
if (path12.length === 6 && path12[0] === "seats" && typeof path12[1] === "number" && path12[2] === "charter" && path12[3] === "permission_manifest" && typeof path12[4] === "number" && path12[5] === "integration_id") {
|
|
36208
36208
|
return true;
|
|
36209
36209
|
}
|
|
36210
|
-
return
|
|
36210
|
+
return path12.length === 6 && path12[0] === "seats" && typeof path12[1] === "number" && path12[2] === "agent" && path12[3] === "skills" && typeof path12[4] === "number" && (path12[5] === "skill_version_id" || path12[5] === "content_sha256");
|
|
36211
36211
|
}
|
|
36212
36212
|
function jsonByteLength2(value) {
|
|
36213
36213
|
try {
|
|
@@ -36217,21 +36217,21 @@ function jsonByteLength2(value) {
|
|
|
36217
36217
|
return Number.POSITIVE_INFINITY;
|
|
36218
36218
|
}
|
|
36219
36219
|
}
|
|
36220
|
-
function prohibitedSetupStringPath(value,
|
|
36220
|
+
function prohibitedSetupStringPath(value, path12 = []) {
|
|
36221
36221
|
if (typeof value === "string") {
|
|
36222
|
-
if (isSchemaProvenMachineString(
|
|
36223
|
-
return hasSecretShapedContent(value) || prohibitedUrlOrPath.test(value) || directContactOrPaymentIdentifier.test(value) ?
|
|
36222
|
+
if (isSchemaProvenMachineString(path12)) return null;
|
|
36223
|
+
return hasSecretShapedContent(value) || prohibitedUrlOrPath.test(value) || directContactOrPaymentIdentifier.test(value) ? path12 : null;
|
|
36224
36224
|
}
|
|
36225
36225
|
if (Array.isArray(value)) {
|
|
36226
36226
|
for (const [index, entry] of value.entries()) {
|
|
36227
|
-
const found = prohibitedSetupStringPath(entry, [...
|
|
36227
|
+
const found = prohibitedSetupStringPath(entry, [...path12, index]);
|
|
36228
36228
|
if (found) return found;
|
|
36229
36229
|
}
|
|
36230
36230
|
return null;
|
|
36231
36231
|
}
|
|
36232
36232
|
if (value && typeof value === "object") {
|
|
36233
36233
|
for (const [key, entry] of Object.entries(value)) {
|
|
36234
|
-
const found = prohibitedSetupStringPath(entry, [...
|
|
36234
|
+
const found = prohibitedSetupStringPath(entry, [...path12, key]);
|
|
36235
36235
|
if (found) return found;
|
|
36236
36236
|
}
|
|
36237
36237
|
}
|
|
@@ -42040,7 +42040,7 @@ var CommandClient = class {
|
|
|
42040
42040
|
this._credentialKind = options.credentialKind;
|
|
42041
42041
|
}
|
|
42042
42042
|
async execute(commandId, body = {}, options = {}) {
|
|
42043
|
-
const
|
|
42043
|
+
const path12 = `/api/commands/${encodeURIComponent(commandId)}`;
|
|
42044
42044
|
const headers = {
|
|
42045
42045
|
authorization: `Bearer ${this.token}`,
|
|
42046
42046
|
"content-type": "application/json"
|
|
@@ -42048,7 +42048,7 @@ var CommandClient = class {
|
|
|
42048
42048
|
if (options.targetSeatId !== void 0 && options.targetSeatId.length > 0) {
|
|
42049
42049
|
headers["x-rost-seat"] = options.targetSeatId;
|
|
42050
42050
|
}
|
|
42051
|
-
const response = await this.fetchImpl(`${this.appUrl}${
|
|
42051
|
+
const response = await this.fetchImpl(`${this.appUrl}${path12}`, {
|
|
42052
42052
|
method: "POST",
|
|
42053
42053
|
headers,
|
|
42054
42054
|
body: JSON.stringify(body)
|
|
@@ -42061,7 +42061,7 @@ var CommandClient = class {
|
|
|
42061
42061
|
const snippet = redactSecrets(text.replace(/\s+/g, " ").trim()).slice(0, 200);
|
|
42062
42062
|
throw new CommandClientError(
|
|
42063
42063
|
response.status,
|
|
42064
|
-
`server returned HTTP ${response.status} (non-JSON) from ${
|
|
42064
|
+
`server returned HTTP ${response.status} (non-JSON) from ${path12}` + (snippet ? ` \u2014 ${snippet}` : "") + " \u2014 the deployment may be unhealthy"
|
|
42065
42065
|
);
|
|
42066
42066
|
}
|
|
42067
42067
|
const parsed = commandResponseSchema.parse(raw);
|
|
@@ -47392,8 +47392,8 @@ var IcebergError = class extends Error {
|
|
|
47392
47392
|
return this.status === 419;
|
|
47393
47393
|
}
|
|
47394
47394
|
};
|
|
47395
|
-
function buildUrl(baseUrl,
|
|
47396
|
-
const url2 = new URL(
|
|
47395
|
+
function buildUrl(baseUrl, path12, query) {
|
|
47396
|
+
const url2 = new URL(path12, baseUrl);
|
|
47397
47397
|
if (query) {
|
|
47398
47398
|
for (const [key, value] of Object.entries(query)) {
|
|
47399
47399
|
if (value !== void 0) {
|
|
@@ -47423,12 +47423,12 @@ function createFetchClient(options) {
|
|
|
47423
47423
|
return {
|
|
47424
47424
|
async request({
|
|
47425
47425
|
method,
|
|
47426
|
-
path:
|
|
47426
|
+
path: path12,
|
|
47427
47427
|
query,
|
|
47428
47428
|
body,
|
|
47429
47429
|
headers
|
|
47430
47430
|
}) {
|
|
47431
|
-
const url2 = buildUrl(options.baseUrl,
|
|
47431
|
+
const url2 = buildUrl(options.baseUrl, path12, query);
|
|
47432
47432
|
const authHeaders = await buildAuthHeaders(options.auth);
|
|
47433
47433
|
const res = await fetchFn(url2, {
|
|
47434
47434
|
method,
|
|
@@ -48290,7 +48290,7 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
48290
48290
|
* @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
|
|
48291
48291
|
* @param fileBody The body of the file to be stored in the bucket.
|
|
48292
48292
|
*/
|
|
48293
|
-
async uploadOrUpdate(method,
|
|
48293
|
+
async uploadOrUpdate(method, path12, fileBody, fileOptions) {
|
|
48294
48294
|
var _this = this;
|
|
48295
48295
|
return _this.handleOperation(async () => {
|
|
48296
48296
|
let body;
|
|
@@ -48314,7 +48314,7 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
48314
48314
|
if ((typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body && typeof body === "object" && "pipe" in body && typeof body.pipe === "function") && !options.duplex) options.duplex = "half";
|
|
48315
48315
|
}
|
|
48316
48316
|
if (fileOptions === null || fileOptions === void 0 ? void 0 : fileOptions.headers) for (const [key, value] of Object.entries(fileOptions.headers)) headers = setHeader(headers, key, value);
|
|
48317
|
-
const cleanPath = _this._removeEmptyFolders(
|
|
48317
|
+
const cleanPath = _this._removeEmptyFolders(path12);
|
|
48318
48318
|
const _path = _this._getFinalPath(cleanPath);
|
|
48319
48319
|
const data = await (method == "PUT" ? put : post)(_this.fetch, `${_this.url}/object/${_path}`, body, _objectSpread22({ headers }, (options === null || options === void 0 ? void 0 : options.duplex) ? { duplex: options.duplex } : {}));
|
|
48320
48320
|
return {
|
|
@@ -48391,8 +48391,8 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
48391
48391
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
48392
48392
|
* - For React Native, using either `Blob`, `File` or `FormData` does not work as intended. Upload file using `ArrayBuffer` from base64 file data instead, see example below.
|
|
48393
48393
|
*/
|
|
48394
|
-
async upload(
|
|
48395
|
-
return this.uploadOrUpdate("POST",
|
|
48394
|
+
async upload(path12, fileBody, fileOptions) {
|
|
48395
|
+
return this.uploadOrUpdate("POST", path12, fileBody, fileOptions);
|
|
48396
48396
|
}
|
|
48397
48397
|
/**
|
|
48398
48398
|
* Upload a file with a token generated from `createSignedUploadUrl`.
|
|
@@ -48432,9 +48432,9 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
48432
48432
|
* - `objects` table permissions: none
|
|
48433
48433
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
48434
48434
|
*/
|
|
48435
|
-
async uploadToSignedUrl(
|
|
48435
|
+
async uploadToSignedUrl(path12, token, fileBody, fileOptions) {
|
|
48436
48436
|
var _this3 = this;
|
|
48437
|
-
const cleanPath = _this3._removeEmptyFolders(
|
|
48437
|
+
const cleanPath = _this3._removeEmptyFolders(path12);
|
|
48438
48438
|
const _path = _this3._getFinalPath(cleanPath);
|
|
48439
48439
|
const url2 = new URL(_this3.url + `/object/upload/sign/${_path}`);
|
|
48440
48440
|
url2.searchParams.set("token", token);
|
|
@@ -48503,10 +48503,10 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
48503
48503
|
* - `objects` table permissions: `insert`
|
|
48504
48504
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
48505
48505
|
*/
|
|
48506
|
-
async createSignedUploadUrl(
|
|
48506
|
+
async createSignedUploadUrl(path12, options) {
|
|
48507
48507
|
var _this4 = this;
|
|
48508
48508
|
return _this4.handleOperation(async () => {
|
|
48509
|
-
let _path = _this4._getFinalPath(
|
|
48509
|
+
let _path = _this4._getFinalPath(path12);
|
|
48510
48510
|
const headers = _objectSpread22({}, _this4.headers);
|
|
48511
48511
|
if (options === null || options === void 0 ? void 0 : options.upsert) headers["x-upsert"] = "true";
|
|
48512
48512
|
const data = await post(_this4.fetch, `${_this4.url}/object/upload/sign/${_path}`, {}, { headers });
|
|
@@ -48515,7 +48515,7 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
48515
48515
|
if (!token) throw new StorageError("No token returned by API");
|
|
48516
48516
|
return {
|
|
48517
48517
|
signedUrl: url2.toString(),
|
|
48518
|
-
path:
|
|
48518
|
+
path: path12,
|
|
48519
48519
|
token
|
|
48520
48520
|
};
|
|
48521
48521
|
});
|
|
@@ -48575,8 +48575,8 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
48575
48575
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
48576
48576
|
* - For React Native, using either `Blob`, `File` or `FormData` does not work as intended. Update file using `ArrayBuffer` from base64 file data instead, see example below.
|
|
48577
48577
|
*/
|
|
48578
|
-
async update(
|
|
48579
|
-
return this.uploadOrUpdate("PUT",
|
|
48578
|
+
async update(path12, fileBody, fileOptions) {
|
|
48579
|
+
return this.uploadOrUpdate("PUT", path12, fileBody, fileOptions);
|
|
48580
48580
|
}
|
|
48581
48581
|
/**
|
|
48582
48582
|
* Moves an existing file to a new path in the same bucket.
|
|
@@ -48727,10 +48727,10 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
48727
48727
|
* - `objects` table permissions: `select`
|
|
48728
48728
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
48729
48729
|
*/
|
|
48730
|
-
async createSignedUrl(
|
|
48730
|
+
async createSignedUrl(path12, expiresIn, options) {
|
|
48731
48731
|
var _this8 = this;
|
|
48732
48732
|
return _this8.handleOperation(async () => {
|
|
48733
|
-
let _path = _this8._getFinalPath(
|
|
48733
|
+
let _path = _this8._getFinalPath(path12);
|
|
48734
48734
|
const hasTransform = typeof (options === null || options === void 0 ? void 0 : options.transform) === "object" && options.transform !== null && Object.keys(options.transform).length > 0;
|
|
48735
48735
|
let data = await post(_this8.fetch, `${_this8.url}/object/sign/${_path}`, _objectSpread22({ expiresIn }, hasTransform ? { transform: options.transform } : {}), { headers: _this8.headers });
|
|
48736
48736
|
const query = new URLSearchParams();
|
|
@@ -48866,13 +48866,13 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
48866
48866
|
* - `objects` table permissions: `select`
|
|
48867
48867
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
48868
48868
|
*/
|
|
48869
|
-
download(
|
|
48869
|
+
download(path12, options, parameters) {
|
|
48870
48870
|
const renderPath = typeof (options === null || options === void 0 ? void 0 : options.transform) === "object" && options.transform !== null && Object.keys(options.transform).length > 0 ? "render/image/authenticated" : "object";
|
|
48871
48871
|
const query = new URLSearchParams();
|
|
48872
48872
|
if (options === null || options === void 0 ? void 0 : options.transform) this.applyTransformOptsToQuery(query, options.transform);
|
|
48873
48873
|
if ((options === null || options === void 0 ? void 0 : options.cacheNonce) != null) query.set("cacheNonce", String(options.cacheNonce));
|
|
48874
48874
|
const queryString = query.toString();
|
|
48875
|
-
const _path = this._getFinalPath(
|
|
48875
|
+
const _path = this._getFinalPath(path12);
|
|
48876
48876
|
const downloadFn = () => get(this.fetch, `${this.url}/${renderPath}/${_path}${queryString ? `?${queryString}` : ""}`, {
|
|
48877
48877
|
headers: this.headers,
|
|
48878
48878
|
noResolveJson: true
|
|
@@ -48903,9 +48903,9 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
48903
48903
|
* }
|
|
48904
48904
|
* ```
|
|
48905
48905
|
*/
|
|
48906
|
-
async info(
|
|
48906
|
+
async info(path12) {
|
|
48907
48907
|
var _this10 = this;
|
|
48908
|
-
const _path = _this10._getFinalPath(
|
|
48908
|
+
const _path = _this10._getFinalPath(path12);
|
|
48909
48909
|
return _this10.handleOperation(async () => {
|
|
48910
48910
|
return recursiveToCamel(await get(_this10.fetch, `${_this10.url}/object/info/${_path}`, { headers: _this10.headers }));
|
|
48911
48911
|
});
|
|
@@ -48926,9 +48926,9 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
48926
48926
|
* .exists('folder/avatar1.png')
|
|
48927
48927
|
* ```
|
|
48928
48928
|
*/
|
|
48929
|
-
async exists(
|
|
48929
|
+
async exists(path12) {
|
|
48930
48930
|
var _this11 = this;
|
|
48931
|
-
const _path = _this11._getFinalPath(
|
|
48931
|
+
const _path = _this11._getFinalPath(path12);
|
|
48932
48932
|
try {
|
|
48933
48933
|
await head(_this11.fetch, `${_this11.url}/object/${_path}`, { headers: _this11.headers });
|
|
48934
48934
|
return {
|
|
@@ -49007,8 +49007,8 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
49007
49007
|
* - `objects` table permissions: none
|
|
49008
49008
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
49009
49009
|
*/
|
|
49010
|
-
getPublicUrl(
|
|
49011
|
-
const _path = this._getFinalPath(
|
|
49010
|
+
getPublicUrl(path12, options) {
|
|
49011
|
+
const _path = this._getFinalPath(path12);
|
|
49012
49012
|
const query = new URLSearchParams();
|
|
49013
49013
|
if (options === null || options === void 0 ? void 0 : options.download) query.set("download", options.download === true ? "" : options.download);
|
|
49014
49014
|
if (options === null || options === void 0 ? void 0 : options.transform) this.applyTransformOptsToQuery(query, options.transform);
|
|
@@ -49147,10 +49147,10 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
49147
49147
|
* - `objects` table permissions: `select`
|
|
49148
49148
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
49149
49149
|
*/
|
|
49150
|
-
async list(
|
|
49150
|
+
async list(path12, options, parameters) {
|
|
49151
49151
|
var _this13 = this;
|
|
49152
49152
|
return _this13.handleOperation(async () => {
|
|
49153
|
-
const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix:
|
|
49153
|
+
const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path12 || "" });
|
|
49154
49154
|
return await post(_this13.fetch, `${_this13.url}/object/list/${_this13.bucketId}`, body, { headers: _this13.headers }, parameters);
|
|
49155
49155
|
});
|
|
49156
49156
|
}
|
|
@@ -49215,11 +49215,11 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
49215
49215
|
if (typeof Buffer !== "undefined") return Buffer.from(data).toString("base64");
|
|
49216
49216
|
return btoa(data);
|
|
49217
49217
|
}
|
|
49218
|
-
_getFinalPath(
|
|
49219
|
-
return `${this.bucketId}/${
|
|
49218
|
+
_getFinalPath(path12) {
|
|
49219
|
+
return `${this.bucketId}/${path12.replace(/^\/+/, "")}`;
|
|
49220
49220
|
}
|
|
49221
|
-
_removeEmptyFolders(
|
|
49222
|
-
return
|
|
49221
|
+
_removeEmptyFolders(path12) {
|
|
49222
|
+
return path12.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
|
|
49223
49223
|
}
|
|
49224
49224
|
/** Modifies the `query`, appending values the from `transform` */
|
|
49225
49225
|
applyTransformOptsToQuery(query, transform2) {
|
|
@@ -53318,7 +53318,7 @@ Canonical source: \`github.com/agenticoperatingsystem/rost-implementation-skill\
|
|
|
53318
53318
|
order: 15,
|
|
53319
53319
|
title: "AICOS chat guide",
|
|
53320
53320
|
summary: "How the AI Chief of Staff chat works in the authenticated app shell and what it can safely do today.",
|
|
53321
|
-
version: "2026-08-03.
|
|
53321
|
+
version: "2026-08-03.5",
|
|
53322
53322
|
public: true,
|
|
53323
53323
|
audiences: ["human", "in_app_agent"],
|
|
53324
53324
|
stages: ["company_setup", "operating_rhythm"],
|
|
@@ -53395,7 +53395,7 @@ The current chat harness shows the effective lane and model. Cloud is the defaul
|
|
|
53395
53395
|
|
|
53396
53396
|
AICOS also has an explicit brain selection. Cloud can use the included managed allowance or a tenant Anthropic key (BYOK). When cloud is on the managed allowance, an owner can also pick a specific managed model from the AI Gateway's priced, ZDR-rated catalog (today: Anthropic's own "Automatic" default, plus managed OpenAI and xAI entries) instead of leaving the choice on Anthropic's default routing. A managed model without a zero-data-retention tier shows as locked with the reason until a tenant owner explicitly unlocks non-ZDR managed models (\`tenant.model_gateway_policy.update\`, owner-only and dangerous-gated); once unlocked, its per-model data-retention disclosure is shown before it becomes selectable. Selecting one of these managed models updates AICOS settings immediately, but it does not yet change which model actually answers a cloud AICOS turn \u2014 every non-Anthropic option says so plainly, and cloud turns keep running on the existing Anthropic routing until a later runtime cutover wires the persisted selection through. Runner brain preference still records Claude or Codex, but interactive AICOS runner turns are currently gated to Claude while the Codex path remains unverified. MCP is labeled as the MCP client. Read this selection with \`aicos.brain_settings.get\`; owners can update it with \`aicos.brain_settings.update\` (CLI flags: \`--lane\`, \`--cloud-brain managed|byok\`, \`--runner-brain claude-cli|codex-cli\`). BYOK requires an active tenant Anthropic key and does not support a specific managed-model selection. Choosing \`codex-cli\` for AICOS runner mode is currently rejected with a precondition error until the governed interactive runner path is verified. Runner and MCP answers are labeled in the transcript and do not consume the managed cloud allowance. If the selected runner reports that its active local model account has exhausted its allowance, Settings surfaces **Allowance exhausted** before the user sends another AICOS turn so they can switch lanes, switch runner accounts, or keep working manually instead of waiting on a doomed runner round-trip.
|
|
53397
53397
|
|
|
53398
|
-
Runner mode uses the same AICOS sessions and transcript. Interactive chat turns are tracked separately from work orders: each user message has one user-owned turn execution with a short deadline, so two users' chat messages cannot collapse into the same work-order slot. Scheduled coordinator work, explicit/manual operator runs, and Forge runner work still use work orders. Runner claim packets carry ids and execution metadata, not the user's free-text request. After claim, the local runner calls the governed AICOS context-load tool, which re-checks seat scope and returns the bounded transcript, session summary, route context, tenant clock, grounding text, and source counts authorized for that turn. For execute-ready runners, the claim also carries a server-built Seat work contract that names the governed runtime profile, the AICOS context-loader tool, and the Seat's permission manifest; action attempts still route through command guards, tool-call audit, and pending confirmations. The runner treats loaded tenant clock, context, and contract as authoritative instead of substituting its own runtime clock, local files, or self-invented authority. A runner must be paired, execute-ready, and backed by the live AICOS agent before it can be selected.
|
|
53398
|
+
Runner mode uses the same AICOS sessions and transcript. Interactive chat turns are tracked separately from work orders: each user message has one user-owned turn execution with a short deadline, so two users' chat messages cannot collapse into the same work-order slot. Scheduled coordinator work, explicit/manual operator runs, and Forge runner work still use work orders. Runner claim packets carry ids and execution metadata, not the user's free-text request. After claim, the local runner calls the governed AICOS context-load tool, which re-checks seat scope and returns the bounded transcript, session summary, route context, tenant clock, grounding text, and source counts authorized for that turn. For execute-ready runners, the claim also carries a server-built Seat work contract that names the governed runtime profile, the AICOS context-loader tool, and the Seat's permission manifest; action attempts still route through command guards, tool-call audit, and pending confirmations. The runner treats loaded tenant clock, context, and contract as authoritative instead of substituting its own runtime clock, local files, or self-invented authority. A runner must be paired, execute-ready, and backed by the live AICOS agent before it can be selected; Claude-backed readiness additionally requires the dedicated cell's truthful \`auth_state: "ready"\` plus \`auth_state_contract_version: 1\` marker. Missing or unknown Claude auth stays visible but is not advertised as ready, and an explicitly Codex-only runner is the only generic-work exception.
|
|
53399
53399
|
|
|
53400
53400
|
Agent seat pages can also expose a direct persistent chat with that seat's active agent. This is different from Seat-scoped AICOS: the target is the agent occupying the seat, the transcript is keyed by tenant, user, purpose, and target agent, and the same user can keep separate active threads for different agents. Owners and admins can read direct-agent transcripts for the tenant, and a human occupying the agent seat's Steward seat can read the transcript; unrelated members cannot. New prompts for a target agent enter an agent-scoped queue with a short turn deadline. A second prompt in the same thread coalesces into the pending turn until that turn is claimed, while prompts for another user or another agent keep their own queue position. Direct agent chat never dispatches through \`work_orders\`; scheduled coordinator work, explicit/manual operator runs, and Forge runner work still use \`work_orders\`.
|
|
53401
53401
|
|
|
@@ -54336,7 +54336,7 @@ External connectors are being rolled out provider by provider, conservatively (r
|
|
|
54336
54336
|
order: 48,
|
|
54337
54337
|
title: "CLI and MCP installation guide",
|
|
54338
54338
|
summary: "Install the public CLI, register remote token-backed MCP clients, and find the full command and tool catalog.",
|
|
54339
|
-
version: "2026-08-04.
|
|
54339
|
+
version: "2026-08-04.3",
|
|
54340
54340
|
public: true,
|
|
54341
54341
|
audiences: ["human", "cli", "mcp", "in_app_agent"],
|
|
54342
54342
|
stages: ["company_setup", "staffing"],
|
|
@@ -54952,7 +54952,7 @@ The signed-in app exposes the same Skill command surface at **Skills**, linked f
|
|
|
54952
54952
|
| \`{{cli}} escalation list|get|resolve|reject\` | \`escalation.list\`, \`escalation.get\`, \`escalation.resolve\`, \`escalation.reject\` | Work the steward escalation queue. | Steward | \`{{cli}} escalation list --json\` |
|
|
54953
54953
|
| \`{{cli}} error list|resolve|supersede\` | \`error_log.list\`, \`error_log.resolve\`, \`error_log.supersede\` | List active, acknowledged, resolved, or superseded product error logs and let a human acknowledge, resolve, or supersede stale errors with evidence links. | Tenant | \`{{cli}} error list --resolved active --json\`; \`{{cli}} error resolve --error-log-id <id> --reason "Fixed in PR #123"\` |
|
|
54954
54954
|
| \`{{cli}} sync brief|compile|complete\` | \`sync.brief.get\`, \`sync.brief.compile\`, \`sync.run.complete\` | Compile, read, and complete a weekly Sync. | Tenant | \`{{cli}} sync brief --json\` |
|
|
54955
|
-
| \`{{cli}} runner list|status|diagnose|repair|work-orders|revoke|serve\` | \`runner.list\`, \`runner.status\`, \`runner.diagnose\`, \`runner.repair\`, \`work_order.list\`, \`runner.revoke\`, runner endpoint APIs | Inspect heartbeat and execute-readiness evidence, inspect work orders, diagnose offline runners, explain claimability gates, get repair guidance, revoke a runner, or run the headless local runner loop. | Tenant | \`{{cli}} runner diagnose --runner-id <id> --json\`; \`{{cli}} runner serve --once\` |
|
|
54955
|
+
| \`{{cli}} runner list|status|diagnose|repair|work-orders|revoke|serve|login\` | \`runner.list\`, \`runner.status\`, \`runner.diagnose\`, \`runner.repair\`, \`work_order.list\`, \`runner.revoke\`, runner endpoint APIs | Inspect heartbeat and execute-readiness evidence, inspect work orders, diagnose offline runners, explain claimability gates, get repair guidance, revoke a runner, or run the headless local runner loop. | Tenant | \`{{cli}} runner diagnose --runner-id <id> --json\`; \`{{cli}} runner serve --once\` |
|
|
54956
54956
|
| \`{{cli}} runner install-service|start|stop|restart|status|logs|uninstall --name <name> [--user-code <code>]\` | launchd/service manager (macOS today) + \`{{cli}} runner serve\` | Install and manage an always-on local runner service that keeps the runner online across reboots. The service runs the runner under the absolute Node binary with an explicit PATH (so it works under nvm/Homebrew) and bootstraps into whichever launchd domain the session supports. Pass \`--user-code\` to pair the service at install; otherwise install prints the exact command to pair it. No secret is stored in the service definition, its arguments, or logs. Non-macOS emits an explicit unsupported message. | Tenant (local machine) | \`{{cli}} runner install-service --name mac-mini --execute --user-code ABCD-EFGH\`; \`{{cli}} runner logs --name mac-mini\` |
|
|
54957
54957
|
| \`{{cli}} notification settings|test|errors\` | \`notification.settings.get\`, \`notification.test\`, \`notification.list_errors\` | Read notification settings, send a test, and list failed deliveries with linked product error source, seat id, and run id when available. | Tenant | \`{{cli}} notification errors --limit 10 --json\` |
|
|
54958
54958
|
| \`{{cli}} integration list|readiness|status|test\` / credential flow for Baserow REST connect | \`integration.connect_rest\`, \`integration.list\`, \`integration.readiness\`, \`integration.status\`, \`integration.test\` | Create a vault-backed Baserow REST integration through the credential flow, list connector metadata, read setup-readiness, read one connector's health, and run provider-specific tests without exposing credentials. | Tenant-admin for connect; tenant for reads/tests | \`{{cli}} integration readiness --provider google --json\` |
|
|
@@ -56709,7 +56709,7 @@ Every notification should include the seat, cause, evidence, and requested decis
|
|
|
56709
56709
|
order: 75,
|
|
56710
56710
|
title: "Local runner guide",
|
|
56711
56711
|
summary: "How local agent sessions and runner surfaces should operate through {{brand}} without bypassing Charters or audit.",
|
|
56712
|
-
version: "2026-08-
|
|
56712
|
+
version: "2026-08-04.1",
|
|
56713
56713
|
public: true,
|
|
56714
56714
|
audiences: ["human", "cli", "mcp", "in_app_agent"],
|
|
56715
56715
|
stages: ["staffing", "operating_rhythm"],
|
|
@@ -56755,9 +56755,13 @@ Runner transport defaults to the canonical production host \`https://rost.elevat
|
|
|
56755
56755
|
|
|
56756
56756
|
Choose the local runtime with \`--runtime auto|claude|codex\` (default \`auto\`). The same short-lived seat-scoped MCP token and server-side guard apply regardless of runtime; results are tagged \`claude-cli\` or \`codex-cli\`.
|
|
56757
56757
|
|
|
56758
|
+
Claude-lane turns authenticate from a dedicated per-runner Claude credential cell, separate from the operator source store. After pairing, run \`{{cli}} runner login\` on the runner host once (if the runner was installed as a service, pass \`--name <runner-name>\` \u2014 with no flags it uses the only paired runner when exactly one exists): it runs \`claude auth login\` scoped to the runner's private credential cell (\`~/.rost/runner/claude-home/\`, keyed on the runner id) and verifies the credential reads back in the exact shape turns use. The credential lives as a file inside that cell \u2014 the same shape on macOS and on Linux. \`runner login\` performs an advisory comparison with your own Claude Code login and discloses a match or an unreadable/null probe; a local comparison cannot prove a runner-only account, so complete the morning second-account proof before production use. Your own Claude credential is unreachable from a turn: the sandbox denies the operator's config dir and the macOS Keychain, so a turn cannot reach it even by re-pointing Claude's credential environment itself. The cell survives restarts, so re-login is only needed after it is deliberately removed. A turn that finds no usable runner credential (never logged in, or the file credential lost) prints a one-line \`{{cli}} runner login\` reminder to the runner's stderr or log; the turn still reports the auth failure.
|
|
56759
|
+
|
|
56758
56760
|
For always-on execution, \`runner serve --execute\` heartbeats on its normal interval and also checks for claimable work between heartbeats. The default interactive claim cadence is about one second; set \`RUNNER_INTERACTIVE_CLAIM_MS\` or \`--interactive-claim-ms\` to tune it for a host. Set how many turns the runner may run in parallel with \`--max-sessions <n>\` or \`RUNNER_MAX_SESSIONS\`; left unset, the runner estimates a value from host CPU and memory. The scheduler honors the declared value (clamped to a small ceiling) so a runner never claims more concurrent work than the capacity it reports. Claude-backed turns reuse a per-seat Claude Code resume session when Claude reports a session id, so later turns can run with \`claude --resume <session-id>\` under the local subscription account. The runner stores only the opaque resume id in its owner-only state file; it does not use API-billed managed sessions to make subscription runner turns faster. A resident long-lived Claude/Codex process is still prototype-gated, so do not assume a fixed latency SLA from this guide alone.
|
|
56759
56761
|
|
|
56760
|
-
|
|
56762
|
+
Claude claims require truthful dedicated-cell evidence: the runner emits \`claude.auth_state_contract_version: 1\` only when its cell probe reports \`installed: true\` and \`auth_state: "ready"\`. The server requires that marker for interactive Claude turns and Claude-backed generic work orders; missing, malformed, or older markers leave work queued while the runner continues heartbeating. A configured Codex-only runner (Claude absent, Codex installed) remains eligible for generic work when it also advertises execution-mode contract v1.
|
|
56763
|
+
|
|
56764
|
+
Each claimed work order carries a server-persisted \`execution_mode\` of \`production_manual\` or \`production_scheduled\`. Current runners validate that field before starting and forward it unchanged into the model context. The server withholds mode-required work from an older runner that does not advertise execution-mode contract v1; upgrade and restart \`{{cli}} runner serve\` to make those queued orders claimable. A new CLI also waits for heartbeat contract v3 before making any claim or resuming an orphaned claim, so rolling against an old or malformed heartbeat route remains heartbeat-only.
|
|
56761
56765
|
|
|
56762
56766
|
The runner bearer secret is stored locally and is never displayed. Each claimed turn receives a short-lived seat-scoped MCP token from {{brand}}, and server-side guards still decide what tools the Seat may use.
|
|
56763
56767
|
|
|
@@ -56788,7 +56792,7 @@ A Forge turn should load its context in one order: build context first, then Ski
|
|
|
56788
56792
|
## Inspect and control runners from CLI or MCP
|
|
56789
56793
|
|
|
56790
56794
|
- Pair a new runner: \`runner.pairing.start\` / \`rost_start_runner_pairing\` with \`name\` and \`platform\` returns a human pairing code. This is owner/admin-only because it mints a short-lived runner pairing session that leads to machine credentials.
|
|
56791
|
-
- Inspect: \`{{cli}} runner list --json\` / \`runner.list\` / \`rost_list_runners\` shows heartbeat state plus execution readiness. A runner is execute-ready only when it has a live heartbeat, a
|
|
56795
|
+
- Inspect: \`{{cli}} runner list --json\` / \`runner.list\` / \`rost_list_runners\` shows heartbeat state plus execution readiness. A runner is execute-ready only when it has a live heartbeat, a claimable local runtime, and recent claim/result evidence or active queue processing. Claude is claimable only with the dedicated-cell \`auth_state: "ready"\` and \`auth_state_contract_version: 1\` marker; an installed-but-unproven Claude CLI remains heartbeat-only, while an explicitly Codex-only configuration may claim generic work. Settings and runner status/list surfaces also project the runner CLI version, sandbox posture, network posture, and reported capability set from heartbeat so operators can see whether the machine is current and still running with the expected guardrails. \`{{cli}} runner status\` / \`runner.status\` / \`rost_runner_status\` reads one runner with the same execution detail, due queue count, claimed/running counts, and recent result timestamps. Both surfaces also project a \`capacity\` view: the runner's active model-account alias, a badge state (\`available\`, \`near_cap\`, \`exhausted\`, or \`unknown\`), a rate-limited-until timestamp when the harness has classified one, and a fresh/stale refreshability flag mirroring heartbeat recency. The badge reads \`unknown\` \u2014 never a fabricated \`available\` \u2014 whenever no capacity observation has been recorded yet or the runner reported a state {{brand}} does not recognize; real 5-hour/weekly usage percentages and reset timestamps are not collected yet, so this view never claims a percentage it does not have. Settings > Connected machines and the Health dashboard's Connectivity tile (a fleet-wide near-cap/at-limit count) surface the same signal.
|
|
56792
56796
|
- Diagnose (DER-949): \`{{cli}} runner diagnose --runner-id <id>\` / \`runner.diagnose\` / \`rost_diagnose_runner\` returns health, capabilities, and repair guidance without exposing secrets. Use when a runner is offline or stale to get actionable next steps.
|
|
56793
56797
|
- Repair steps (DER-949): \`{{cli}} runner repair --runner-id <id> [--issue restart|re_pair|revoke_stale|missing_cli_runtime]\` / \`runner.repair\` / \`rost_repair_runner\` returns focused repair instructions. The optional \`issue\` flag narrows to a specific kind of repair (restart the app, re-pair, revoke a stale runner, or install a missing CLI runtime).
|
|
56794
56798
|
- Work orders: \`{{cli}} runner work-orders\` / \`work_order.list\` / \`rost_list_work_orders\`; queue with \`work_order.enqueue\` / \`rost_enqueue_work_order\` for an execution-ready live scheduled agent, or use \`agent.run_now\` / \`rost_run_agent_now\` when an operator wants the product to queue and dispatch a manual production run (\`production_manual\`) from a seat id; cancel with \`work_order.cancel\` / \`rost_cancel_work_order\`. Both issuance paths pass through the same central \`execution_ready\` gate. The operator Settings page mirrors queued, claimed, running, and recent completed runner evidence so online does not imply can execute. \`task_id\` on either enqueue command is cloud-lane only: a runner-lane agent rejects it, because a task-linked runner work order is structurally unclaimable \u2014 run task-linked runner work as an interactive agent turn (see Interactive AICOS runner turns above), not through the scheduled work-order lane.
|
|
@@ -62175,7 +62179,7 @@ function operationUsageLines(bin) {
|
|
|
62175
62179
|
`${bin} escalation list|get|resolve|reject`,
|
|
62176
62180
|
`${bin} error list|resolve|supersede`,
|
|
62177
62181
|
`${bin} sync brief|compile|complete`,
|
|
62178
|
-
`${bin} runner list|status|diagnose|repair|work-orders|revoke|serve`,
|
|
62182
|
+
`${bin} runner list|status|diagnose|repair|work-orders|revoke|serve|login`,
|
|
62179
62183
|
// Local runner service lifecycle (DER-1142). The orchestrator mirrors this
|
|
62180
62184
|
// exact line into the mcp-and-cli-guide so the help/guide parity gate passes.
|
|
62181
62185
|
`${bin} runner install-service|start|stop|restart|status|logs|uninstall --name <name>`,
|
|
@@ -63848,6 +63852,9 @@ function runnerCodexHomeBaseDir(homeDir) {
|
|
|
63848
63852
|
return path6.join(homeDir, ".rost", "runner", "codex-home");
|
|
63849
63853
|
}
|
|
63850
63854
|
var LEGACY_TMP_CODEX_HOME_DIR = path6.join(tmpdir(), "rost-runner-codex-home");
|
|
63855
|
+
function runnerClaudeHomeBaseDir(homeDir) {
|
|
63856
|
+
return path6.join(homeDir, ".rost", "runner", "claude-home");
|
|
63857
|
+
}
|
|
63851
63858
|
var codexHomeBaseCanonicalCache = /* @__PURE__ */ new Map();
|
|
63852
63859
|
function canonicalCodexHomeBase(homeDir) {
|
|
63853
63860
|
const key = path6.resolve(homeDir);
|
|
@@ -63859,11 +63866,23 @@ function canonicalCodexHomeBase(homeDir) {
|
|
|
63859
63866
|
codexHomeBaseCanonicalCache.set(key, resolved);
|
|
63860
63867
|
return resolved;
|
|
63861
63868
|
}
|
|
63869
|
+
var claudeHomeBaseCanonicalCache = /* @__PURE__ */ new Map();
|
|
63870
|
+
function canonicalClaudeHomeBase(homeDir) {
|
|
63871
|
+
const key = path6.resolve(homeDir);
|
|
63872
|
+
const cached2 = claudeHomeBaseCanonicalCache.get(key);
|
|
63873
|
+
if (cached2 !== void 0) {
|
|
63874
|
+
return cached2;
|
|
63875
|
+
}
|
|
63876
|
+
const resolved = canonicalDeep(runnerClaudeHomeBaseDir(key));
|
|
63877
|
+
claudeHomeBaseCanonicalCache.set(key, resolved);
|
|
63878
|
+
return resolved;
|
|
63879
|
+
}
|
|
63862
63880
|
var RUNNER_TEMP_PREFIXES = [
|
|
63863
63881
|
"rost-runner-mcp-",
|
|
63864
63882
|
"rost-runner-sandbox-",
|
|
63865
63883
|
"rost-runner-askpass-",
|
|
63866
|
-
"rost-runner-claude-config-"
|
|
63884
|
+
"rost-runner-claude-config-",
|
|
63885
|
+
"rost-runner-login-verify-"
|
|
63867
63886
|
];
|
|
63868
63887
|
var RUNNER_TEMP_STARTUP_SWEEP_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
63869
63888
|
var RUNNER_TEMP_TURN_SWEEP_MAX_AGE_MS = 45 * 60 * 1e3;
|
|
@@ -63942,13 +63961,25 @@ var STRICT_SYSTEM_READ_SUBPATHS = [
|
|
|
63942
63961
|
"/opt",
|
|
63943
63962
|
"/nix"
|
|
63944
63963
|
];
|
|
63964
|
+
var KEYCHAIN_MACH_SERVICES = [
|
|
63965
|
+
"com.apple.SecurityServer",
|
|
63966
|
+
"com.apple.securityd.xpc",
|
|
63967
|
+
"com.apple.security.agent",
|
|
63968
|
+
"com.apple.security.XPCKeychainSandboxCheck"
|
|
63969
|
+
];
|
|
63945
63970
|
function buildSeatbeltProfile(input) {
|
|
63946
63971
|
const home = input.homeDir.replace(/\/+$/, "");
|
|
63947
63972
|
const lines = ["(version 1)"];
|
|
63948
63973
|
const claudeSource = input.claudeSourceHome ?? `${home}/.claude`;
|
|
63949
63974
|
const codexSource = input.codexSourceHome ?? `${home}/.codex`;
|
|
63950
|
-
const
|
|
63951
|
-
const
|
|
63975
|
+
const claudeSecureStorageSource = input.claudeSecureStorageSourceHome ?? claudeSource;
|
|
63976
|
+
const runtimeHomeAllows = [];
|
|
63977
|
+
const runtimeHomeDenies = Array.from(/* @__PURE__ */ new Set([codexSource, claudeSource, claudeSecureStorageSource])).filter(
|
|
63978
|
+
(p) => !runtimeHomeAllows.includes(p)
|
|
63979
|
+
);
|
|
63980
|
+
const claudeSourceSiblingJsonDenies = Array.from(/* @__PURE__ */ new Set([claudeSource, claudeSecureStorageSource])).map(
|
|
63981
|
+
(dir) => literal2(`${dir}.json`)
|
|
63982
|
+
);
|
|
63952
63983
|
const denyReadBaseSubpaths = input.denyReadPaths.filter((p) => p.endsWith("/")).map((p) => subpath(p.replace(/\/+$/, "")));
|
|
63953
63984
|
const denyReadLiterals = input.denyReadPaths.filter((p) => !p.endsWith("/")).map(literal2);
|
|
63954
63985
|
const denyWriteSubpaths = input.denyWritePaths.filter((p) => p.endsWith("/")).map((p) => subpath(p.replace(/\/+$/, "")));
|
|
@@ -63958,8 +63989,11 @@ function buildSeatbeltProfile(input) {
|
|
|
63958
63989
|
...SENSITIVE_READ_LITERALS.map((rel) => literal2(`${home}/${rel}`))
|
|
63959
63990
|
];
|
|
63960
63991
|
const projectCodexDir = input.modelRuntime === "codex" && input.workspaceDir !== input.tmpDir ? `${stripTrailingSlash(input.workspaceDir)}/.codex` : null;
|
|
63961
|
-
const
|
|
63962
|
-
const
|
|
63992
|
+
const codexIsolateRoot = stripTrailingSlash(canonicalCodexHomeBase(input.homeDir));
|
|
63993
|
+
const claudeIsolateRoot = stripTrailingSlash(canonicalClaudeHomeBase(input.homeDir));
|
|
63994
|
+
const allowLast = (paths) => (paths ?? []).map(stripTrailingSlash).filter(
|
|
63995
|
+
(p) => p.length > 0 && p !== codexIsolateRoot && p !== claudeIsolateRoot && (isWithin(p, codexIsolateRoot) || isWithin(p, claudeIsolateRoot))
|
|
63996
|
+
);
|
|
63963
63997
|
const allowReadLast = allowLast(input.allowReadLastSubpaths);
|
|
63964
63998
|
const allowWriteLast = allowLast(input.allowWriteLastSubpaths);
|
|
63965
63999
|
if (input.strict) {
|
|
@@ -63980,9 +64014,10 @@ function buildSeatbeltProfile(input) {
|
|
|
63980
64014
|
// every child path stays individually deny-gated. (Guard never hits this branch.)
|
|
63981
64015
|
literal2("/"),
|
|
63982
64016
|
...STRICT_SYSTEM_READ_SUBPATHS.map(subpath),
|
|
63983
|
-
// DER-
|
|
63984
|
-
//
|
|
63985
|
-
//
|
|
64017
|
+
// DER-3022: the operator SOURCE config dirs are NOT read-allowed for any runtime. The turn's
|
|
64018
|
+
// only readable+writable config home is its per-turn ISOLATE, re-allowed as the absolute-last
|
|
64019
|
+
// rule below. Keeping the (now-empty) `runtimeHomeAllows` spread as a belt: a future change
|
|
64020
|
+
// that reintroduces a per-runtime source allow will not silently drop it from strict mode.
|
|
63986
64021
|
...runtimeHomeAllows.map(subpath),
|
|
63987
64022
|
subpath(input.tmpDir)
|
|
63988
64023
|
];
|
|
@@ -64021,9 +64056,12 @@ function buildSeatbeltProfile(input) {
|
|
|
64021
64056
|
...credentialReadDenies,
|
|
64022
64057
|
...denyReadLiterals,
|
|
64023
64058
|
...input.denyReadLastSubpaths.map((p) => subpath(p.replace(/\/+$/, ""))),
|
|
64024
|
-
// DER-1485: the
|
|
64025
|
-
// under guard's allow-default reads (and over strict's system/home allow above).
|
|
64059
|
+
// DER-1485 / DER-3022: the operator runtime-credential SOURCE stores — denied last-wins so it
|
|
64060
|
+
// holds even under guard's allow-default reads (and over strict's system/home allow above).
|
|
64026
64061
|
...runtimeHomeDenies.map(subpath),
|
|
64062
|
+
// DER-3022 (kickback round 3, P2): the claude source dirs' SIBLING `.json` files (e.g.
|
|
64063
|
+
// `~/.claude.json` beside `~/.claude`) — a `subpath` deny on the directory does not cover them.
|
|
64064
|
+
...claudeSourceSiblingJsonDenies,
|
|
64027
64065
|
...projectCodexDenies
|
|
64028
64066
|
];
|
|
64029
64067
|
lines.push(`(deny file-read* ${lastReadDenies.join(" ")})`);
|
|
@@ -64038,12 +64076,15 @@ function buildSeatbeltProfile(input) {
|
|
|
64038
64076
|
// `.git/{objects,refs,worktrees,logs,index,HEAD}` stay writable for local commits.
|
|
64039
64077
|
...denyWriteSubpaths,
|
|
64040
64078
|
...denyWriteLiterals,
|
|
64041
|
-
// DER-1485: the
|
|
64079
|
+
// DER-1485 / DER-3022: the operator runtime-credential SOURCE stores are never writable by this
|
|
64080
|
+
// turn.
|
|
64042
64081
|
...runtimeHomeDenies.map(subpath),
|
|
64043
|
-
// DER-
|
|
64044
|
-
|
|
64045
|
-
//
|
|
64046
|
-
//
|
|
64082
|
+
// DER-3022 (kickback round 3, P2): same sibling-`.json` gap as the read deny above, for write.
|
|
64083
|
+
...claudeSourceSiblingJsonDenies,
|
|
64084
|
+
// DER-1485 (round 5 P2) / DER-3022: the operator SOURCE config dirs are read-only seed input —
|
|
64085
|
+
// explicitly deny writes to them last-wins, so a workspace-write turn cannot plant settings/hooks
|
|
64086
|
+
// into the operator's persistent CLI config. (This deny never overlaps a write re-allow, since
|
|
64087
|
+
// runtimeHomeAllows are no longer write-allowed.)
|
|
64047
64088
|
...runtimeHomeAllows.map(subpath),
|
|
64048
64089
|
// DER-1485 (round 3): a codex turn must not write a hostile checked-in `.codex/config.toml`
|
|
64049
64090
|
// (a non-seat MCP a trusted project would then load and shell out to) either.
|
|
@@ -64054,6 +64095,9 @@ function buildSeatbeltProfile(input) {
|
|
|
64054
64095
|
lines.push(`(allow file-write* ${allowWriteLast.map(subpath).join(" ")})`);
|
|
64055
64096
|
}
|
|
64056
64097
|
lines.push('(deny mach-lookup (global-name "com.apple.coreservices.appleevents"))');
|
|
64098
|
+
for (const service of KEYCHAIN_MACH_SERVICES) {
|
|
64099
|
+
lines.push(`(deny mach-lookup (global-name "${service}"))`);
|
|
64100
|
+
}
|
|
64057
64101
|
if (!input.allowNetwork) {
|
|
64058
64102
|
lines.push("(deny network*)");
|
|
64059
64103
|
} else if (input.strict) {
|
|
@@ -64120,24 +64164,34 @@ function resolveSandboxSpec(options) {
|
|
|
64120
64164
|
);
|
|
64121
64165
|
denyWritePaths.push(`${canonicalFile(RUNNER_ASKPASS_DIR)}/`);
|
|
64122
64166
|
denyWritePaths.push(`${canonicalCodexHomeBase(options.homeDir)}/`);
|
|
64167
|
+
denyWritePaths.push(`${canonicalClaudeHomeBase(options.homeDir)}/`);
|
|
64123
64168
|
denyWritePaths.push(`${canonicalDeep(LEGACY_TMP_CODEX_HOME_DIR)}/`);
|
|
64124
64169
|
const allowWritePaths = (options.allowWritePaths ?? []).map(canonical);
|
|
64125
64170
|
const denyReadLastSubpaths = [
|
|
64126
64171
|
`${canonicalFile(RUNNER_ASKPASS_DIR)}/`,
|
|
64127
|
-
// DER-1485 (round 5/6): the harness-private
|
|
64128
|
-
// CODEX_HOME, so denying
|
|
64129
|
-
// token and keeps the turn from pre-planting a home path
|
|
64130
|
-
// This turn's OWN isolate is re-allowed after this deny
|
|
64131
|
-
//
|
|
64172
|
+
// DER-1485 (round 5/6) / DER-3022: the harness-private runtime-home BASES. They hold every
|
|
64173
|
+
// runner's isolated CODEX_HOME / CLAUDE_CONFIG_DIR cell, so denying each base keeps one runner's
|
|
64174
|
+
// turn out of a SIBLING runner's operator token and keeps the turn from pre-planting a home path
|
|
64175
|
+
// the harness would later seed into. This turn's OWN isolate is re-allowed after this deny
|
|
64176
|
+
// (`codexIsolateHomeDir`/`claudeIsolateHomeDir` below) — it must be, because the runtime reads its
|
|
64177
|
+
// own credentials under the SAME profile its Bash child runs under.
|
|
64132
64178
|
//
|
|
64133
|
-
// DER-2298 (finding 3): the DURABLE
|
|
64134
|
-
// VOLATILE base —
|
|
64135
|
-
// whether it holds this runner's own home, a sibling runner's, or a pre-move leftover.
|
|
64179
|
+
// DER-2298 / DER-3022 (finding 3): the DURABLE bases (functions of `homeDir` now) plus the
|
|
64180
|
+
// round-5/6 VOLATILE codex base — all denied for the same reason: none may be read by a sandboxed
|
|
64181
|
+
// turn, whether it holds this runner's own home, a sibling runner's, or a pre-move leftover.
|
|
64136
64182
|
`${canonicalCodexHomeBase(options.homeDir)}/`,
|
|
64183
|
+
`${canonicalClaudeHomeBase(options.homeDir)}/`,
|
|
64137
64184
|
`${canonicalDeep(LEGACY_TMP_CODEX_HOME_DIR)}/`
|
|
64138
64185
|
];
|
|
64139
64186
|
const codexIsolate = options.codexIsolateHomeDir ? path6.join(canonicalCodexHomeBase(options.homeDir), path6.basename(options.codexIsolateHomeDir)) : null;
|
|
64140
|
-
const
|
|
64187
|
+
const claudeIsolate = options.claudeIsolateHomeDir ? path6.join(canonicalClaudeHomeBase(options.homeDir), path6.basename(options.claudeIsolateHomeDir)) : null;
|
|
64188
|
+
const allowLastSubpaths = [];
|
|
64189
|
+
if (codexIsolate !== null && options.modelRuntime === "codex") {
|
|
64190
|
+
allowLastSubpaths.push(codexIsolate);
|
|
64191
|
+
}
|
|
64192
|
+
if (claudeIsolate !== null && options.modelRuntime === "claude") {
|
|
64193
|
+
allowLastSubpaths.push(claudeIsolate);
|
|
64194
|
+
}
|
|
64141
64195
|
if (codexIsolate !== null) {
|
|
64142
64196
|
const legacyFlatHome = path6.join(path6.dirname(canonicalDeep(LEGACY_TMP_CODEX_HOME_DIR)), path6.basename(codexIsolate));
|
|
64143
64197
|
denyReadLastSubpaths.push(`${legacyFlatHome}/`);
|
|
@@ -64157,7 +64211,8 @@ function resolveSandboxSpec(options) {
|
|
|
64157
64211
|
strict: options.mode === "strict",
|
|
64158
64212
|
...options.modelRuntime ? { modelRuntime: options.modelRuntime } : {},
|
|
64159
64213
|
...options.claudeSourceHome ? { claudeSourceHome: canonical(options.claudeSourceHome) } : {},
|
|
64160
|
-
...options.codexSourceHome ? { codexSourceHome: canonical(options.codexSourceHome) } : {}
|
|
64214
|
+
...options.codexSourceHome ? { codexSourceHome: canonical(options.codexSourceHome) } : {},
|
|
64215
|
+
...options.claudeSecureStorageSourceHome ? { claudeSecureStorageSourceHome: canonical(options.claudeSecureStorageSourceHome) } : {}
|
|
64161
64216
|
});
|
|
64162
64217
|
return {
|
|
64163
64218
|
kind: "seatbelt",
|
|
@@ -65058,15 +65113,13 @@ function parseSemver2(value) {
|
|
|
65058
65113
|
};
|
|
65059
65114
|
}
|
|
65060
65115
|
|
|
65061
|
-
// src/runner-
|
|
65062
|
-
import {
|
|
65063
|
-
import {
|
|
65064
|
-
import { constants as
|
|
65065
|
-
import {
|
|
65066
|
-
import
|
|
65067
|
-
import
|
|
65068
|
-
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
65069
|
-
import { promisify as promisify5 } from "node:util";
|
|
65116
|
+
// src/runner-login.ts
|
|
65117
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
65118
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
65119
|
+
import { constants as fsConstants5 } from "node:fs";
|
|
65120
|
+
import { lstat as lstat3, mkdtemp as mkdtemp2, readdir as readdir5, realpath as realpath2, rm as rm7, writeFile as writeFile7 } from "node:fs/promises";
|
|
65121
|
+
import os3 from "node:os";
|
|
65122
|
+
import path11 from "node:path";
|
|
65070
65123
|
|
|
65071
65124
|
// src/forge-workspace.ts
|
|
65072
65125
|
import { execFile as execFileCallback4 } from "node:child_process";
|
|
@@ -65140,11 +65193,14 @@ var execFile4 = promisify4(execFileCallback4);
|
|
|
65140
65193
|
var MODEL_SPAWN_ENV_ALLOWLIST = [
|
|
65141
65194
|
"PATH",
|
|
65142
65195
|
"HOME",
|
|
65143
|
-
//
|
|
65144
|
-
//
|
|
65145
|
-
//
|
|
65146
|
-
//
|
|
65147
|
-
//
|
|
65196
|
+
// Retained as a non-secret identity var some child tools still consult. It is no longer
|
|
65197
|
+
// load-bearing for Claude Code auth: Claude Code v2 resolves its credential from the secure
|
|
65198
|
+
// store scoped by `CLAUDE_SECURESTORAGE_CONFIG_DIR` (or, absent that, by `CLAUDE_CONFIG_DIR`)
|
|
65199
|
+
// — measured on claude 2.1.220/macOS: this is the `.credentials.json` file inside that
|
|
65200
|
+
// directory, read straight off disk on macOS exactly as on Linux, never looked up by `$USER`;
|
|
65201
|
+
// `claude auth status` succeeds with `USER` unset (DER-3022). The v1 single-item-Keychain
|
|
65202
|
+
// assumption is retired — the runner's turn sandbox denies the Keychain outright, so the file
|
|
65203
|
+
// is the only credential store a turn can reach on either OS.
|
|
65148
65204
|
"USER",
|
|
65149
65205
|
"LANG",
|
|
65150
65206
|
"LC_ALL",
|
|
@@ -65152,7 +65208,7 @@ var MODEL_SPAWN_ENV_ALLOWLIST = [
|
|
|
65152
65208
|
"SHELL",
|
|
65153
65209
|
"NODE_OPTIONS"
|
|
65154
65210
|
];
|
|
65155
|
-
function buildModelSpawnEnv(base, claudeConfigDir, codexHomeDir) {
|
|
65211
|
+
function buildModelSpawnEnv(base, claudeConfigDir, codexHomeDir, claudeSecureStorageDir) {
|
|
65156
65212
|
const out = {};
|
|
65157
65213
|
for (const key of MODEL_SPAWN_ENV_ALLOWLIST) {
|
|
65158
65214
|
const value = base[key];
|
|
@@ -65167,9 +65223,36 @@ function buildModelSpawnEnv(base, claudeConfigDir, codexHomeDir) {
|
|
|
65167
65223
|
if (typeof codexHomeDir === "string" && codexHomeDir.length > 0) {
|
|
65168
65224
|
out.CODEX_HOME = codexHomeDir;
|
|
65169
65225
|
}
|
|
65226
|
+
delete out.CLAUDE_SECURESTORAGE_CONFIG_DIR;
|
|
65227
|
+
if (typeof claudeSecureStorageDir === "string" && claudeSecureStorageDir.length > 0) {
|
|
65228
|
+
out.CLAUDE_SECURESTORAGE_CONFIG_DIR = claudeSecureStorageDir;
|
|
65229
|
+
}
|
|
65170
65230
|
return out;
|
|
65171
65231
|
}
|
|
65172
|
-
var
|
|
65232
|
+
var CLAUDE_CONFIG_SEED_FILE = ".claude.json";
|
|
65233
|
+
var CLAUDE_CONFIG_SEED_ALLOWLIST_KEYS = [
|
|
65234
|
+
"hasCompletedOnboarding",
|
|
65235
|
+
"theme",
|
|
65236
|
+
"userID",
|
|
65237
|
+
"oauthAccount"
|
|
65238
|
+
];
|
|
65239
|
+
function sanitizeClaudeConfigJson(data) {
|
|
65240
|
+
try {
|
|
65241
|
+
const parsed = JSON.parse(data.toString("utf8"));
|
|
65242
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
65243
|
+
return null;
|
|
65244
|
+
}
|
|
65245
|
+
const sanitized = {};
|
|
65246
|
+
for (const key of CLAUDE_CONFIG_SEED_ALLOWLIST_KEYS) {
|
|
65247
|
+
if (key in parsed) {
|
|
65248
|
+
sanitized[key] = parsed[key];
|
|
65249
|
+
}
|
|
65250
|
+
}
|
|
65251
|
+
return sanitized;
|
|
65252
|
+
} catch {
|
|
65253
|
+
return null;
|
|
65254
|
+
}
|
|
65255
|
+
}
|
|
65173
65256
|
function resolveSourceClaudeConfigDir(base) {
|
|
65174
65257
|
const explicit = requireNonBlank("CLAUDE_CONFIG_DIR", base.CLAUDE_CONFIG_DIR);
|
|
65175
65258
|
if (explicit !== null) {
|
|
@@ -65179,18 +65262,212 @@ function resolveSourceClaudeConfigDir(base) {
|
|
|
65179
65262
|
const homeDir = typeof home === "string" && home.length > 0 ? home : os2.homedir();
|
|
65180
65263
|
return path8.join(homeDir, ".claude");
|
|
65181
65264
|
}
|
|
65182
|
-
|
|
65265
|
+
function resolveSourceClaudeSecureStorageDir(base) {
|
|
65266
|
+
const explicit = requireNonBlank("CLAUDE_SECURESTORAGE_CONFIG_DIR", base.CLAUDE_SECURESTORAGE_CONFIG_DIR);
|
|
65267
|
+
if (explicit !== null) {
|
|
65268
|
+
return explicit;
|
|
65269
|
+
}
|
|
65270
|
+
return resolveSourceClaudeConfigDir(base);
|
|
65271
|
+
}
|
|
65272
|
+
async function provisionIsolatedClaudeConfigDir(configDir, seedSourceDir, onRepair) {
|
|
65183
65273
|
await ensureVerifiedPrivateDir(configDir, "claude config dir", onRepair);
|
|
65184
|
-
const source =
|
|
65185
|
-
|
|
65186
|
-
|
|
65187
|
-
|
|
65188
|
-
|
|
65189
|
-
|
|
65274
|
+
const source = seedSourceDir;
|
|
65275
|
+
await ensureVerifiedCodexHomeDir(source, onRepair, "claude credential cell");
|
|
65276
|
+
const seedRead = await readSeedFile(source, CLAUDE_CONFIG_SEED_FILE);
|
|
65277
|
+
if (seedRead.status === "ok") {
|
|
65278
|
+
const sanitized = sanitizeClaudeConfigJson(seedRead.data);
|
|
65279
|
+
if (sanitized !== null) {
|
|
65280
|
+
await writeFileAtomicNoFollow(
|
|
65281
|
+
path8.join(configDir, CLAUDE_CONFIG_SEED_FILE),
|
|
65282
|
+
Buffer.from(JSON.stringify(sanitized), "utf8"),
|
|
65283
|
+
{ mode: 384 }
|
|
65284
|
+
);
|
|
65285
|
+
} else {
|
|
65286
|
+
onRepair?.(`claude config dir seed: ${CLAUDE_CONFIG_SEED_FILE} in ${source} was not a plain JSON object \u2014 skipped`);
|
|
65190
65287
|
}
|
|
65288
|
+
} else if (seedRead.status === "rejected") {
|
|
65289
|
+
onRepair?.(`claude config dir seed: ${CLAUDE_CONFIG_SEED_FILE} in ${source} was not a plain file owned by this user (possible attack) \u2014 skipped`);
|
|
65191
65290
|
}
|
|
65192
65291
|
return configDir;
|
|
65193
65292
|
}
|
|
65293
|
+
async function sweepClaudeCredentialCell(cell2, onRepair) {
|
|
65294
|
+
for (const entry of await readdir3(cell2)) {
|
|
65295
|
+
const entryPath = path8.join(cell2, entry);
|
|
65296
|
+
if (entry === ".credentials.json" || entry === ".claude.json") {
|
|
65297
|
+
const info = await lstat(entryPath).catch(() => null);
|
|
65298
|
+
if (info !== null && info.isFile() && !info.isSymbolicLink()) {
|
|
65299
|
+
continue;
|
|
65300
|
+
}
|
|
65301
|
+
}
|
|
65302
|
+
let looksLikeOwnOrphanedTemp = false;
|
|
65303
|
+
if (CODEX_HOME_TMP_NAME_RE.test(entry)) {
|
|
65304
|
+
const entryInfo = await lstat(entryPath).catch(() => null);
|
|
65305
|
+
looksLikeOwnOrphanedTemp = entryInfo !== null && entryInfo.isFile();
|
|
65306
|
+
}
|
|
65307
|
+
let looksLikeOwnClaudeBookkeeping = false;
|
|
65308
|
+
if (!looksLikeOwnOrphanedTemp && CLAUDE_CELL_BENIGN_ENTRIES.has(entry)) {
|
|
65309
|
+
const entryInfo = await lstat(entryPath).catch(() => null);
|
|
65310
|
+
looksLikeOwnClaudeBookkeeping = entryInfo !== null && !entryInfo.isSymbolicLink() && (entryInfo.isFile() || entryInfo.isDirectory());
|
|
65311
|
+
}
|
|
65312
|
+
if (!looksLikeOwnOrphanedTemp && !looksLikeOwnClaudeBookkeeping) {
|
|
65313
|
+
onRepair?.(`claude credential cell ${cell2}: removing unexpected entry ${JSON.stringify(entry)}`);
|
|
65314
|
+
}
|
|
65315
|
+
try {
|
|
65316
|
+
await rm5(entryPath, { recursive: true, force: true });
|
|
65317
|
+
} catch (error51) {
|
|
65318
|
+
const code = error51.code ?? "unknown";
|
|
65319
|
+
throw new Error(
|
|
65320
|
+
`refusing to run claude runner login: could not remove ${JSON.stringify(entry)} from the claude credential cell ${cell2} (${code}). The credential-cell sweep must not fail open \u2014 clear the entry (e.g. \`chflags -R nouchg\`, then \`rm -rf\`) and retry.`
|
|
65321
|
+
);
|
|
65322
|
+
}
|
|
65323
|
+
}
|
|
65324
|
+
const configRead = await readSeedFile(cell2, CLAUDE_CONFIG_SEED_FILE);
|
|
65325
|
+
const credentialsRead = await readSeedFile(cell2, ".credentials.json");
|
|
65326
|
+
const configPath = path8.join(cell2, CLAUDE_CONFIG_SEED_FILE);
|
|
65327
|
+
const credentialsPath = path8.join(cell2, ".credentials.json");
|
|
65328
|
+
if (configRead.status === "rejected") {
|
|
65329
|
+
onRepair?.(
|
|
65330
|
+
`claude credential cell ${cell2}: ${CLAUDE_CONFIG_SEED_FILE} was not a plain file owned by this user (possible attack) \u2014 removed`
|
|
65331
|
+
);
|
|
65332
|
+
try {
|
|
65333
|
+
await rm5(configPath, { force: true });
|
|
65334
|
+
} catch (error51) {
|
|
65335
|
+
const code = error51.code ?? "unknown";
|
|
65336
|
+
throw new Error(
|
|
65337
|
+
`refusing to run claude runner login: could not remove ${JSON.stringify(CLAUDE_CONFIG_SEED_FILE)} from the claude credential cell ${cell2} (${code}). The credential-cell sweep must not fail open \u2014 clear the entry (e.g. \`chflags -R nouchg\`, then \`rm -rf\`) and retry.`
|
|
65338
|
+
);
|
|
65339
|
+
}
|
|
65340
|
+
} else if (configRead.status === "ok") {
|
|
65341
|
+
const sanitized = sanitizeClaudeConfigJson(configRead.data);
|
|
65342
|
+
if (sanitized !== null) {
|
|
65343
|
+
await writeFileAtomicNoFollow(configPath, Buffer.from(JSON.stringify(sanitized), "utf8"), { mode: 384 });
|
|
65344
|
+
} else {
|
|
65345
|
+
onRepair?.(`claude credential cell ${cell2}: ${CLAUDE_CONFIG_SEED_FILE} was not a plain JSON object \u2014 removed`);
|
|
65346
|
+
await rm5(configPath, { force: true });
|
|
65347
|
+
}
|
|
65348
|
+
}
|
|
65349
|
+
if (credentialsRead.status === "rejected") {
|
|
65350
|
+
onRepair?.(
|
|
65351
|
+
`claude credential cell ${cell2}: .credentials.json was not a plain file owned by this user (possible attack) \u2014 removed`
|
|
65352
|
+
);
|
|
65353
|
+
try {
|
|
65354
|
+
await rm5(credentialsPath, { force: true });
|
|
65355
|
+
} catch (error51) {
|
|
65356
|
+
const code = error51.code ?? "unknown";
|
|
65357
|
+
throw new Error(
|
|
65358
|
+
`refusing to run claude runner login: could not remove ".credentials.json" from the claude credential cell ${cell2} (${code}). The credential-cell sweep must not fail open \u2014 clear the entry (e.g. \`chflags -R nouchg\`, then \`rm -rf\`) and retry.`
|
|
65359
|
+
);
|
|
65360
|
+
}
|
|
65361
|
+
}
|
|
65362
|
+
}
|
|
65363
|
+
async function readSeedFile(source, file2) {
|
|
65364
|
+
const handle = await open(path8.join(source, file2), fsConstants3.O_RDONLY | fsConstants3.O_NOFOLLOW).catch(() => null);
|
|
65365
|
+
if (handle === null) {
|
|
65366
|
+
return { status: "absent" };
|
|
65367
|
+
}
|
|
65368
|
+
try {
|
|
65369
|
+
const info = await handle.stat();
|
|
65370
|
+
const usable = info.isFile() && info.nlink === 1 && info.size > 0 && (typeof process.getuid !== "function" || info.uid === process.getuid());
|
|
65371
|
+
if (!usable) {
|
|
65372
|
+
return { status: "rejected" };
|
|
65373
|
+
}
|
|
65374
|
+
return { status: "ok", data: await handle.readFile() };
|
|
65375
|
+
} finally {
|
|
65376
|
+
await handle.close();
|
|
65377
|
+
}
|
|
65378
|
+
}
|
|
65379
|
+
async function hasUsableClaudeCredentialFile(cell2) {
|
|
65380
|
+
const read = await readSeedFile(cell2, ".credentials.json");
|
|
65381
|
+
if (read.status !== "ok") {
|
|
65382
|
+
return false;
|
|
65383
|
+
}
|
|
65384
|
+
return parseUsableClaudeCredentialJson(read.data.toString("utf8")) !== null;
|
|
65385
|
+
}
|
|
65386
|
+
function claudeKeychainServiceNameForSecureStorageDir(secureStorageDir) {
|
|
65387
|
+
const digest = createHash3("sha256").update(secureStorageDir).digest("hex").slice(0, 8);
|
|
65388
|
+
return `Claude Code-credentials-${digest}`;
|
|
65389
|
+
}
|
|
65390
|
+
function parseUsableClaudeCredentialJson(raw) {
|
|
65391
|
+
try {
|
|
65392
|
+
const parsed = JSON.parse(raw);
|
|
65393
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
65394
|
+
return null;
|
|
65395
|
+
}
|
|
65396
|
+
const oauth = parsed.claudeAiOauth;
|
|
65397
|
+
if (!oauth || typeof oauth !== "object" || Array.isArray(oauth)) {
|
|
65398
|
+
return null;
|
|
65399
|
+
}
|
|
65400
|
+
const accessToken = oauth.accessToken;
|
|
65401
|
+
if (typeof accessToken !== "string" || accessToken.trim().length === 0) {
|
|
65402
|
+
return null;
|
|
65403
|
+
}
|
|
65404
|
+
return JSON.stringify(parsed);
|
|
65405
|
+
} catch {
|
|
65406
|
+
return null;
|
|
65407
|
+
}
|
|
65408
|
+
}
|
|
65409
|
+
async function materializeClaudeCredentialFileFromMacosKeychain(cell2, options = {}) {
|
|
65410
|
+
const platform2 = options.platform ?? process.platform;
|
|
65411
|
+
if (platform2 !== "darwin") {
|
|
65412
|
+
return { status: "unavailable", reason: "not_darwin" };
|
|
65413
|
+
}
|
|
65414
|
+
if (await hasUsableClaudeCredentialFile(cell2)) {
|
|
65415
|
+
return { status: "already_present" };
|
|
65416
|
+
}
|
|
65417
|
+
const serviceName3 = claudeKeychainServiceNameForSecureStorageDir(cell2);
|
|
65418
|
+
const readKeychainSecret = options.readKeychainSecretImpl ?? readClaudeKeychainSecret;
|
|
65419
|
+
let raw;
|
|
65420
|
+
try {
|
|
65421
|
+
raw = await readKeychainSecret(serviceName3);
|
|
65422
|
+
} catch (error51) {
|
|
65423
|
+
return {
|
|
65424
|
+
status: "unavailable",
|
|
65425
|
+
reason: `keychain_read_failed:${redactSecrets(error51 instanceof Error ? error51.message : String(error51))}`
|
|
65426
|
+
};
|
|
65427
|
+
}
|
|
65428
|
+
if (raw === null) {
|
|
65429
|
+
return { status: "unavailable", reason: "keychain_item_missing" };
|
|
65430
|
+
}
|
|
65431
|
+
const normalized = parseUsableClaudeCredentialJson(raw.trim());
|
|
65432
|
+
if (normalized === null) {
|
|
65433
|
+
return { status: "unavailable", reason: "keychain_payload_unusable" };
|
|
65434
|
+
}
|
|
65435
|
+
const dest = path8.join(cell2, ".credentials.json");
|
|
65436
|
+
try {
|
|
65437
|
+
await writeFileAtomicNoFollow(dest, `${normalized}
|
|
65438
|
+
`, { mode: 384 });
|
|
65439
|
+
} catch (error51) {
|
|
65440
|
+
return {
|
|
65441
|
+
status: "unavailable",
|
|
65442
|
+
reason: `credential_file_write_failed:${redactSecrets(error51 instanceof Error ? error51.message : String(error51))}`
|
|
65443
|
+
};
|
|
65444
|
+
}
|
|
65445
|
+
if (!await hasUsableClaudeCredentialFile(cell2)) {
|
|
65446
|
+
return { status: "unavailable", reason: "credential_file_unreadable_after_write" };
|
|
65447
|
+
}
|
|
65448
|
+
return { status: "written" };
|
|
65449
|
+
}
|
|
65450
|
+
async function readClaudeKeychainSecret(serviceName3) {
|
|
65451
|
+
try {
|
|
65452
|
+
const { stdout } = await execFile4("security", ["find-generic-password", "-s", serviceName3, "-w"], {
|
|
65453
|
+
encoding: "utf8",
|
|
65454
|
+
timeout: 1e4,
|
|
65455
|
+
maxBuffer: 1024 * 1024
|
|
65456
|
+
});
|
|
65457
|
+
const trimmed = stdout.trim();
|
|
65458
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
65459
|
+
} catch (error51) {
|
|
65460
|
+
const code = error51.status ?? error51.code;
|
|
65461
|
+
if (code === 44) {
|
|
65462
|
+
return null;
|
|
65463
|
+
}
|
|
65464
|
+
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
65465
|
+
if (/could not be found|SecKeychainSearchCopyNext/i.test(message)) {
|
|
65466
|
+
return null;
|
|
65467
|
+
}
|
|
65468
|
+
throw error51;
|
|
65469
|
+
}
|
|
65470
|
+
}
|
|
65194
65471
|
var CODEX_HOME_AUTH_FILE = "auth.json";
|
|
65195
65472
|
var CODEX_HOME_CONFIG_FILE = "config.toml";
|
|
65196
65473
|
function resolveSourceCodexHomeDir(base) {
|
|
@@ -65204,6 +65481,7 @@ function resolveSourceCodexHomeDir(base) {
|
|
|
65204
65481
|
}
|
|
65205
65482
|
var CODEX_HOME_TMP_PREFIX = ".rost-tmp-";
|
|
65206
65483
|
var CODEX_HOME_TMP_NAME_RE = /^\.rost-tmp-[0-9a-f]{24}$/;
|
|
65484
|
+
var CLAUDE_CELL_BENIGN_ENTRIES = /* @__PURE__ */ new Set([".claude.json.lock", "backups"]);
|
|
65207
65485
|
async function writeFileAtomicNoFollow(dest, data, options = {}) {
|
|
65208
65486
|
const mode = options.mode ?? 384;
|
|
65209
65487
|
const durable = options.durable ?? true;
|
|
@@ -65260,24 +65538,24 @@ async function writeFileAtomicNoFollow(dest, data, options = {}) {
|
|
|
65260
65538
|
await rm5(tmp, { force: true }).catch(() => void 0);
|
|
65261
65539
|
}
|
|
65262
65540
|
}
|
|
65263
|
-
async function ensureVerifiedCodexHomeBase(baseDir) {
|
|
65541
|
+
async function ensureVerifiedCodexHomeBase(baseDir, label = "codex home base") {
|
|
65264
65542
|
const preInfo = await lstat(baseDir).catch(() => null);
|
|
65265
65543
|
if (preInfo !== null && (preInfo.isSymbolicLink() || !preInfo.isDirectory())) {
|
|
65266
|
-
throw new Error(`refusing to use
|
|
65544
|
+
throw new Error(`refusing to use ${label} ${baseDir}: not a real directory (possible symlink attack)`);
|
|
65267
65545
|
}
|
|
65268
65546
|
await mkdir5(baseDir, { recursive: true, mode: 448 });
|
|
65269
65547
|
const expected = path8.join(await realpath(path8.dirname(baseDir)), path8.basename(baseDir));
|
|
65270
65548
|
const info = await lstat(baseDir);
|
|
65271
65549
|
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
65272
|
-
throw new Error(`refusing to use
|
|
65550
|
+
throw new Error(`refusing to use ${label} ${baseDir}: not a real directory (possible symlink attack)`);
|
|
65273
65551
|
}
|
|
65274
65552
|
if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
|
|
65275
|
-
throw new Error(`refusing to use
|
|
65553
|
+
throw new Error(`refusing to use ${label} ${baseDir}: not owned by this user (possible pre-planted dir)`);
|
|
65276
65554
|
}
|
|
65277
65555
|
await chmod(baseDir, 448);
|
|
65278
65556
|
const resolved = await realpath(baseDir);
|
|
65279
65557
|
if (resolved !== expected) {
|
|
65280
|
-
throw new Error(`refusing to use
|
|
65558
|
+
throw new Error(`refusing to use ${label} ${baseDir}: resolves to ${resolved} (possible symlink attack)`);
|
|
65281
65559
|
}
|
|
65282
65560
|
return resolved;
|
|
65283
65561
|
}
|
|
@@ -65303,26 +65581,53 @@ async function ensureVerifiedPrivateDir(dir, label, onRepair) {
|
|
|
65303
65581
|
return;
|
|
65304
65582
|
}
|
|
65305
65583
|
}
|
|
65306
|
-
async function ensureVerifiedCodexHomeDir(homeDir, onRepair) {
|
|
65307
|
-
const base = await ensureVerifiedCodexHomeBase(path8.dirname(path8.resolve(homeDir)));
|
|
65584
|
+
async function ensureVerifiedCodexHomeDir(homeDir, onRepair, label = "codex home") {
|
|
65585
|
+
const base = await ensureVerifiedCodexHomeBase(path8.dirname(path8.resolve(homeDir)), `${label} base`);
|
|
65308
65586
|
const expected = path8.join(base, path8.basename(homeDir));
|
|
65309
|
-
await ensureVerifiedPrivateDir(homeDir,
|
|
65587
|
+
await ensureVerifiedPrivateDir(homeDir, label, onRepair);
|
|
65310
65588
|
if (await realpath(homeDir) !== expected) {
|
|
65311
|
-
throw new Error(`refusing to use
|
|
65589
|
+
throw new Error(`refusing to use ${label} ${homeDir}: resolved outside ${expected} (possible symlink attack)`);
|
|
65312
65590
|
}
|
|
65313
65591
|
return expected;
|
|
65314
65592
|
}
|
|
65593
|
+
async function isVerifiedExistingCodexHomeDir(homeDir) {
|
|
65594
|
+
const resolvedHome = path8.resolve(homeDir);
|
|
65595
|
+
const base = path8.dirname(resolvedHome);
|
|
65596
|
+
const [baseInfo, homeInfo] = await Promise.all([
|
|
65597
|
+
lstat(base).catch(() => null),
|
|
65598
|
+
lstat(resolvedHome).catch(() => null)
|
|
65599
|
+
]);
|
|
65600
|
+
const ownedDirectory = (info) => info !== null && info.isDirectory() && !info.isSymbolicLink() && (typeof process.getuid !== "function" || info.uid === process.getuid());
|
|
65601
|
+
if (!ownedDirectory(baseInfo) || !ownedDirectory(homeInfo)) {
|
|
65602
|
+
return false;
|
|
65603
|
+
}
|
|
65604
|
+
const [baseParentReal, baseReal, homeReal] = await Promise.all([
|
|
65605
|
+
realpath(path8.dirname(base)).catch(() => null),
|
|
65606
|
+
realpath(base).catch(() => null),
|
|
65607
|
+
realpath(resolvedHome).catch(() => null)
|
|
65608
|
+
]);
|
|
65609
|
+
if (baseParentReal === null || baseReal === null || homeReal === null) {
|
|
65610
|
+
return false;
|
|
65611
|
+
}
|
|
65612
|
+
return baseReal === path8.join(baseParentReal, path8.basename(base)) && homeReal === path8.join(baseReal, path8.basename(resolvedHome));
|
|
65613
|
+
}
|
|
65315
65614
|
function codexHomeSeedMarkerPath(homeDir) {
|
|
65316
65615
|
return path8.join(path8.dirname(homeDir), `${path8.basename(homeDir)}.seeded`);
|
|
65317
65616
|
}
|
|
65318
|
-
async function commitCodexHomeSeedMarker(homeDir) {
|
|
65617
|
+
async function commitCodexHomeSeedMarker(homeDir, options = {}) {
|
|
65319
65618
|
const marker = codexHomeSeedMarkerPath(homeDir);
|
|
65320
|
-
const
|
|
65321
|
-
|
|
65619
|
+
const existing = await lstat(marker).catch(() => null);
|
|
65620
|
+
const usableExisting = existing !== null && existing.isFile() && !existing.isSymbolicLink() && existing.nlink === 1 && (typeof process.getuid !== "function" || existing.uid === process.getuid());
|
|
65621
|
+
if (existing !== null && (!options.required || usableExisting)) {
|
|
65322
65622
|
return;
|
|
65323
65623
|
}
|
|
65324
|
-
|
|
65325
|
-
`, { mode: 384 })
|
|
65624
|
+
const write = writeFileAtomicNoFollow(marker, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
65625
|
+
`, { mode: 384 });
|
|
65626
|
+
if (options.required) {
|
|
65627
|
+
await write;
|
|
65628
|
+
return;
|
|
65629
|
+
}
|
|
65630
|
+
await write.catch(() => void 0);
|
|
65326
65631
|
}
|
|
65327
65632
|
async function provisionIsolatedCodexHomeDir(base, homeDir, projectRoot, onRepair) {
|
|
65328
65633
|
await ensureVerifiedCodexHomeDir(homeDir, onRepair);
|
|
@@ -65415,14 +65720,15 @@ trust_level = "untrusted"
|
|
|
65415
65720
|
}
|
|
65416
65721
|
}
|
|
65417
65722
|
}
|
|
65418
|
-
function
|
|
65723
|
+
function credentialCellBusySummary(waitedMs, runtime) {
|
|
65419
65724
|
const waitedSeconds = Math.max(0, Math.round(waitedMs / 1e3));
|
|
65420
|
-
|
|
65725
|
+
const cellLabel = runtime === "codex" ? "the isolated CODEX_HOME" : "the isolated claude credential cell";
|
|
65726
|
+
return `refusing to run: another ${runtime} turn on this runner holds ${cellLabel}. The credential there is single-use and rotates on refresh, so two concurrent turns would both exchange it and leave the runner permanently unauthenticated. Waited ${waitedSeconds}s and it is still held; this is transient \u2014 retry the unit.`;
|
|
65421
65727
|
}
|
|
65422
|
-
var CODEX_HOME_BUSY_SUMMARY = codexHomeBusySummary(0);
|
|
65423
65728
|
var CODEX_HOME_LOCK_FORMING_GRACE_MS = 5e3;
|
|
65424
65729
|
async function acquireCodexHomeLock(homeDir, options = {}) {
|
|
65425
|
-
const
|
|
65730
|
+
const label = options.label ?? "codex home";
|
|
65731
|
+
const base = await ensureVerifiedCodexHomeBase(path8.dirname(path8.resolve(homeDir)), `${label} base`);
|
|
65426
65732
|
const lockPath = path8.join(base, `${path8.basename(homeDir)}.lock`);
|
|
65427
65733
|
const waitMs = options.waitMs ?? 30 * 6e4;
|
|
65428
65734
|
const staleMs = options.staleMs ?? 60 * 6e4;
|
|
@@ -66299,6 +66605,16 @@ async function removeWorkspace(ws) {
|
|
|
66299
66605
|
await removeWorktree(ws.baseRepo, ws.worktreeDir);
|
|
66300
66606
|
}
|
|
66301
66607
|
|
|
66608
|
+
// src/runner-serve.ts
|
|
66609
|
+
import { execFile as execFileCallback5, spawn } from "node:child_process";
|
|
66610
|
+
import { createHash as createHash4, randomUUID } from "node:crypto";
|
|
66611
|
+
import { constants as fsConstants4 } from "node:fs";
|
|
66612
|
+
import { access as access3, chmod as chmod2, lstat as lstat2, mkdir as mkdir6, open as open2, readFile as readFile9, rename as rename2, rm as rm6, statfs, writeFile as writeFile6 } from "node:fs/promises";
|
|
66613
|
+
import { cpus, homedir as homedir5, hostname as hostname3, platform, totalmem, tmpdir as tmpdir2 } from "node:os";
|
|
66614
|
+
import path10 from "node:path";
|
|
66615
|
+
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
66616
|
+
import { promisify as promisify5 } from "node:util";
|
|
66617
|
+
|
|
66302
66618
|
// src/host-resource.ts
|
|
66303
66619
|
import { execFileSync } from "node:child_process";
|
|
66304
66620
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
@@ -66623,10 +66939,12 @@ ${usage()}
|
|
|
66623
66939
|
let failed = false;
|
|
66624
66940
|
let activeSessions = 0;
|
|
66625
66941
|
let reconciled = false;
|
|
66942
|
+
let deferredReconcileWorkOrderIds = null;
|
|
66626
66943
|
const activeTurnExecutionIds = /* @__PURE__ */ new Set();
|
|
66627
66944
|
let serverHeartbeatContractVersion = null;
|
|
66945
|
+
let serverClaimContractVersion = null;
|
|
66628
66946
|
do {
|
|
66629
|
-
const detectedCapabilities = await detectCapabilities(cliVersion, env, homeDir, config2.maxSessionsOverride);
|
|
66947
|
+
const detectedCapabilities = await detectCapabilities(cliVersion, env, homeDir, state, config2.maxSessionsOverride);
|
|
66630
66948
|
const capabilities = capabilitiesForConfiguredRuntime(detectedCapabilities, config2.runtime);
|
|
66631
66949
|
const localRuntime = config2.execute ? effectiveExecutionRuntime(config2.runtime, capabilities) : null;
|
|
66632
66950
|
try {
|
|
@@ -66638,6 +66956,7 @@ ${usage()}
|
|
|
66638
66956
|
}), state.runner_secret);
|
|
66639
66957
|
const heartbeatOk = heartbeat.status === 200;
|
|
66640
66958
|
if (!heartbeatOk) {
|
|
66959
|
+
serverClaimContractVersion = null;
|
|
66641
66960
|
options.io.stderr.write(`heartbeat ${heartbeat.status}: ${redactForLog(JSON.stringify(heartbeat.json))}
|
|
66642
66961
|
`);
|
|
66643
66962
|
failed = true;
|
|
@@ -66647,6 +66966,7 @@ ${usage()}
|
|
|
66647
66966
|
if (learnedVersion !== null) {
|
|
66648
66967
|
serverHeartbeatContractVersion = learnedVersion;
|
|
66649
66968
|
}
|
|
66969
|
+
serverClaimContractVersion = learnedVersion !== null && learnedVersion >= HEARTBEAT_CLAIM_MIN_CONTRACT_VERSION ? learnedVersion : null;
|
|
66650
66970
|
options.io.stdout.write(`heartbeat ok #${beat} runner_id=${state.runner_id} claude=${capabilities.claude.installed} codex=${capabilities.codex.installed}
|
|
66651
66971
|
`);
|
|
66652
66972
|
}
|
|
@@ -66667,32 +66987,57 @@ ${usage()}
|
|
|
66667
66987
|
}
|
|
66668
66988
|
});
|
|
66669
66989
|
try {
|
|
66670
|
-
if (!reconciled && heartbeatOk) {
|
|
66990
|
+
if (!reconciled && heartbeatOk && serverSupportsRunnerClaimContract(serverClaimContractVersion)) {
|
|
66991
|
+
let reconcileOutcome = { deferredWorkOrderIds: [] };
|
|
66671
66992
|
try {
|
|
66672
|
-
await reconcileOrphanedInFlightWorkOrders({
|
|
66993
|
+
reconcileOutcome = await reconcileOrphanedInFlightWorkOrders({
|
|
66994
|
+
fetchImpl,
|
|
66995
|
+
appUrl: options.appUrl,
|
|
66996
|
+
state,
|
|
66997
|
+
config: config2,
|
|
66998
|
+
io: options.io,
|
|
66999
|
+
allowResume: true,
|
|
67000
|
+
localRuntime,
|
|
67001
|
+
onlyWorkOrderIds: deferredReconcileWorkOrderIds
|
|
67002
|
+
});
|
|
66673
67003
|
} catch (error51) {
|
|
66674
67004
|
options.io.stderr.write(`reconcile orphaned work orders failed (continuing): ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
|
|
66675
67005
|
`);
|
|
66676
67006
|
} finally {
|
|
66677
|
-
|
|
67007
|
+
deferredReconcileWorkOrderIds = reconcileOutcome.deferredWorkOrderIds.length > 0 ? new Set(reconcileOutcome.deferredWorkOrderIds) : null;
|
|
67008
|
+
reconciled = reconcileOutcome.deferredWorkOrderIds.length === 0;
|
|
66678
67009
|
}
|
|
66679
67010
|
}
|
|
66680
|
-
|
|
67011
|
+
if (serverSupportsRunnerClaimContract(serverClaimContractVersion) && deferredReconcileWorkOrderIds === null) {
|
|
67012
|
+
await claimAndExecute(fetchImpl, options.appUrl, state, config2, options.io, localRuntime, activeTurnExecutionIds);
|
|
67013
|
+
}
|
|
66681
67014
|
} finally {
|
|
66682
67015
|
stopTurnHeartbeat();
|
|
66683
67016
|
activeSessions = Math.max(0, activeSessions - 1);
|
|
66684
67017
|
}
|
|
66685
67018
|
} else if (!reconciled && heartbeatOk) {
|
|
67019
|
+
let reconcileOutcome = { deferredWorkOrderIds: [] };
|
|
66686
67020
|
try {
|
|
66687
|
-
await reconcileOrphanedInFlightWorkOrders({
|
|
67021
|
+
reconcileOutcome = await reconcileOrphanedInFlightWorkOrders({
|
|
67022
|
+
fetchImpl,
|
|
67023
|
+
appUrl: options.appUrl,
|
|
67024
|
+
state,
|
|
67025
|
+
config: config2,
|
|
67026
|
+
io: options.io,
|
|
67027
|
+
allowResume: false,
|
|
67028
|
+
localRuntime: null,
|
|
67029
|
+
onlyWorkOrderIds: deferredReconcileWorkOrderIds
|
|
67030
|
+
});
|
|
66688
67031
|
} catch (error51) {
|
|
66689
67032
|
options.io.stderr.write(`reconcile orphaned work orders failed (continuing): ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
|
|
66690
67033
|
`);
|
|
66691
67034
|
} finally {
|
|
66692
|
-
|
|
67035
|
+
deferredReconcileWorkOrderIds = reconcileOutcome.deferredWorkOrderIds.length > 0 ? new Set(reconcileOutcome.deferredWorkOrderIds) : null;
|
|
67036
|
+
reconciled = reconcileOutcome.deferredWorkOrderIds.length === 0;
|
|
66693
67037
|
}
|
|
66694
67038
|
}
|
|
66695
67039
|
} catch (error51) {
|
|
67040
|
+
serverClaimContractVersion = null;
|
|
66696
67041
|
failed = true;
|
|
66697
67042
|
options.io.stderr.write(`transient runner error (continuing): ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
|
|
66698
67043
|
`);
|
|
@@ -66715,13 +67060,17 @@ ${usage()}
|
|
|
66715
67060
|
() => serverHeartbeatContractVersion,
|
|
66716
67061
|
(v) => {
|
|
66717
67062
|
serverHeartbeatContractVersion = v;
|
|
67063
|
+
},
|
|
67064
|
+
() => serverClaimContractVersion,
|
|
67065
|
+
(v) => {
|
|
67066
|
+
serverClaimContractVersion = v;
|
|
66718
67067
|
}
|
|
66719
67068
|
);
|
|
66720
67069
|
}
|
|
66721
67070
|
} while (!config2.once);
|
|
66722
67071
|
return heartbeatSucceeded && !failed ? 0 : 1;
|
|
66723
67072
|
}
|
|
66724
|
-
async function waitForNextHeartbeat(fetchImpl, appUrl2, state, config2, io, runtime, capabilities, telemetry, activeTurnExecutionIds,
|
|
67073
|
+
async function waitForNextHeartbeat(fetchImpl, appUrl2, state, config2, io, runtime, capabilities, telemetry, activeTurnExecutionIds, serverHeartbeatContractVersion, recordServerHeartbeatContractVersion, serverClaimContractVersion, recordServerClaimContractVersion) {
|
|
66725
67074
|
const deadline = Date.now() + config2.heartbeatMs;
|
|
66726
67075
|
const stopWait = runtime ? startTurnHeartbeat({
|
|
66727
67076
|
fetchImpl,
|
|
@@ -66732,14 +67081,15 @@ async function waitForNextHeartbeat(fetchImpl, appUrl2, state, config2, io, runt
|
|
|
66732
67081
|
io,
|
|
66733
67082
|
telemetry,
|
|
66734
67083
|
activeTurnIds: () => [...activeTurnExecutionIds],
|
|
66735
|
-
serverContractVersion,
|
|
66736
|
-
recordServerContractVersion
|
|
67084
|
+
serverContractVersion: serverHeartbeatContractVersion,
|
|
67085
|
+
recordServerContractVersion: recordServerHeartbeatContractVersion,
|
|
67086
|
+
recordClaimContractVersion: recordServerClaimContractVersion
|
|
66737
67087
|
}) : null;
|
|
66738
67088
|
try {
|
|
66739
67089
|
while (Date.now() < deadline) {
|
|
66740
67090
|
const intervalMs = Math.min(config2.interactiveClaimMs, deadline - Date.now());
|
|
66741
67091
|
await sleep3(intervalMs);
|
|
66742
|
-
if (!runtime || Date.now() >= deadline) {
|
|
67092
|
+
if (!runtime || Date.now() >= deadline || !serverSupportsRunnerClaimContract(serverClaimContractVersion())) {
|
|
66743
67093
|
continue;
|
|
66744
67094
|
}
|
|
66745
67095
|
try {
|
|
@@ -66754,6 +67104,10 @@ async function waitForNextHeartbeat(fetchImpl, appUrl2, state, config2, io, runt
|
|
|
66754
67104
|
}
|
|
66755
67105
|
}
|
|
66756
67106
|
var HEARTBEAT_ACTIVE_TURN_MIN_CONTRACT_VERSION = 2;
|
|
67107
|
+
var HEARTBEAT_CLAIM_MIN_CONTRACT_VERSION = 3;
|
|
67108
|
+
function serverSupportsRunnerClaimContract(version4) {
|
|
67109
|
+
return version4 !== null && version4 >= HEARTBEAT_CLAIM_MIN_CONTRACT_VERSION;
|
|
67110
|
+
}
|
|
66757
67111
|
function readHeartbeatContractVersion(json2) {
|
|
66758
67112
|
const value = json2.heartbeat_contract_version;
|
|
66759
67113
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
@@ -66788,6 +67142,7 @@ function startTurnHeartbeat(deps) {
|
|
|
66788
67142
|
serverContractVersion: deps.serverContractVersion()
|
|
66789
67143
|
}), deps.secret, controller.signal);
|
|
66790
67144
|
if (!stopped && response.status !== 200) {
|
|
67145
|
+
deps.recordClaimContractVersion?.(null);
|
|
66791
67146
|
deps.io.stderr.write(`turn heartbeat ${response.status}: ${redactForLog(JSON.stringify(response.json))}
|
|
66792
67147
|
`);
|
|
66793
67148
|
} else if (!stopped && response.status === 200) {
|
|
@@ -66795,8 +67150,14 @@ function startTurnHeartbeat(deps) {
|
|
|
66795
67150
|
if (learnedVersion !== null) {
|
|
66796
67151
|
deps.recordServerContractVersion(learnedVersion);
|
|
66797
67152
|
}
|
|
67153
|
+
deps.recordClaimContractVersion?.(
|
|
67154
|
+
learnedVersion !== null && learnedVersion >= HEARTBEAT_CLAIM_MIN_CONTRACT_VERSION ? learnedVersion : null
|
|
67155
|
+
);
|
|
66798
67156
|
}
|
|
66799
67157
|
} catch (error51) {
|
|
67158
|
+
if (!stopped) {
|
|
67159
|
+
deps.recordClaimContractVersion?.(null);
|
|
67160
|
+
}
|
|
66800
67161
|
if (!stopped) {
|
|
66801
67162
|
deps.io.stderr.write(`turn heartbeat error (continuing): ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
|
|
66802
67163
|
`);
|
|
@@ -66848,7 +67209,7 @@ function detectTelemetry(capabilities, env, activeSessions, cliVersion) {
|
|
|
66848
67209
|
}
|
|
66849
67210
|
function parseServeArgs(args, appUrl2, env, homeDir) {
|
|
66850
67211
|
let name = env.RUNNER_NAME ?? `headless-${hostname3()}`;
|
|
66851
|
-
let stateFile = env.RUNNER_STATE_FILE ??
|
|
67212
|
+
let stateFile = env.RUNNER_STATE_FILE ?? defaultRunnerStateFile(appUrl2);
|
|
66852
67213
|
let heartbeatMs = Number(env.RUNNER_HEARTBEAT_MS ?? 3e4);
|
|
66853
67214
|
let interactiveClaimMs = Number(env.RUNNER_INTERACTIVE_CLAIM_MS ?? 1e3);
|
|
66854
67215
|
let execute2 = env.RUNNER_EXECUTE === "1";
|
|
@@ -67021,10 +67382,14 @@ async function pairIfNeeded(client, fetchImpl, appUrl2, config2, io) {
|
|
|
67021
67382
|
};
|
|
67022
67383
|
await saveState(config2.stateFile, state);
|
|
67023
67384
|
if (priorState && priorState.runner_id !== runnerId) {
|
|
67024
|
-
const
|
|
67025
|
-
await rm6(
|
|
67026
|
-
await rm6(`${
|
|
67027
|
-
await rm6(`${
|
|
67385
|
+
const supersededCodexHome = stableCodexHomeDir(priorState, config2.homeDir);
|
|
67386
|
+
await rm6(supersededCodexHome, { recursive: true, force: true }).catch(() => void 0);
|
|
67387
|
+
await rm6(`${supersededCodexHome}.seeded`, { force: true }).catch(() => void 0);
|
|
67388
|
+
await rm6(`${supersededCodexHome}.lock`, { force: true }).catch(() => void 0);
|
|
67389
|
+
const supersededClaudeHome = stableClaudeHomeDir(priorState, config2.homeDir);
|
|
67390
|
+
await rm6(supersededClaudeHome, { recursive: true, force: true }).catch(() => void 0);
|
|
67391
|
+
await rm6(`${supersededClaudeHome}.seeded`, { force: true }).catch(() => void 0);
|
|
67392
|
+
await rm6(`${supersededClaudeHome}.lock`, { force: true }).catch(() => void 0);
|
|
67028
67393
|
}
|
|
67029
67394
|
await migrateLegacyCodexHome(state, config2.homeDir, io);
|
|
67030
67395
|
io.stdout.write(`paired runner_id=${runnerId} state_file=${config2.stateFile}
|
|
@@ -67091,6 +67456,9 @@ async function migrateOneLegacyCodexHome(legacyHome, newHome, io) {
|
|
|
67091
67456
|
}
|
|
67092
67457
|
}
|
|
67093
67458
|
}
|
|
67459
|
+
function defaultRunnerStateFile(appUrl2) {
|
|
67460
|
+
return path10.join(tmpdir2(), `rost-runner-${createHash4("sha1").update(appUrl2).digest("hex").slice(0, 8)}.json`);
|
|
67461
|
+
}
|
|
67094
67462
|
async function loadState(filePath, expectedTenantId) {
|
|
67095
67463
|
try {
|
|
67096
67464
|
const parsed = JSON.parse(await readFile9(filePath, "utf8"));
|
|
@@ -67152,9 +67520,9 @@ function estimateMaxSessions(cpuCount, memoryMb) {
|
|
|
67152
67520
|
const estimate = Math.min(byCpu, byMem);
|
|
67153
67521
|
return Math.min(Math.max(estimate, 1), RUNNER_MAX_SESSIONS_CAP);
|
|
67154
67522
|
}
|
|
67155
|
-
async function detectCapabilities(cliVersion, env, homeDir, maxSessionsOverride) {
|
|
67523
|
+
async function detectCapabilities(cliVersion, env, homeDir, state, maxSessionsOverride) {
|
|
67156
67524
|
const [claude, codex, git, gh, forgeDiskFreeMb] = await Promise.all([
|
|
67157
|
-
probeClaude(),
|
|
67525
|
+
probeClaude(state, homeDir),
|
|
67158
67526
|
probe("codex"),
|
|
67159
67527
|
probeGit(),
|
|
67160
67528
|
probeGh(),
|
|
@@ -67252,7 +67620,7 @@ async function probe(command) {
|
|
|
67252
67620
|
return { installed: false, version: null, auth_state: "unknown" };
|
|
67253
67621
|
}
|
|
67254
67622
|
}
|
|
67255
|
-
async function probeClaude() {
|
|
67623
|
+
async function probeClaude(state, homeDir) {
|
|
67256
67624
|
const base = await probe("claude");
|
|
67257
67625
|
if (!base.installed) {
|
|
67258
67626
|
return {
|
|
@@ -67266,20 +67634,32 @@ async function probeClaude() {
|
|
|
67266
67634
|
}
|
|
67267
67635
|
};
|
|
67268
67636
|
}
|
|
67637
|
+
const cell2 = stableClaudeHomeDir(state, homeDir);
|
|
67269
67638
|
const streamJsonSupported = await probeClaudeStreamJsonInterface();
|
|
67639
|
+
const residentSession = streamJsonSupported ? {
|
|
67640
|
+
status: "prototype_gated",
|
|
67641
|
+
transport: "stream-json",
|
|
67642
|
+
reason: "resident session gated until multi-turn behavior proven"
|
|
67643
|
+
} : {
|
|
67644
|
+
status: "unsupported",
|
|
67645
|
+
transport: null,
|
|
67646
|
+
reason: "stream-json interface not detected on this runner"
|
|
67647
|
+
};
|
|
67648
|
+
if (await claudeCellNeedsLogin(cell2)) {
|
|
67649
|
+
return {
|
|
67650
|
+
...base,
|
|
67651
|
+
auth_state: "unknown",
|
|
67652
|
+
account_alias: null,
|
|
67653
|
+
rate_limited_until: null,
|
|
67654
|
+
resident_session: residentSession
|
|
67655
|
+
};
|
|
67656
|
+
}
|
|
67270
67657
|
return {
|
|
67271
67658
|
...base,
|
|
67659
|
+
auth_state_contract_version: 1,
|
|
67272
67660
|
account_alias: null,
|
|
67273
67661
|
rate_limited_until: null,
|
|
67274
|
-
resident_session:
|
|
67275
|
-
status: "prototype_gated",
|
|
67276
|
-
transport: "stream-json",
|
|
67277
|
-
reason: "resident session gated until multi-turn behavior proven"
|
|
67278
|
-
} : {
|
|
67279
|
-
status: "unsupported",
|
|
67280
|
-
transport: null,
|
|
67281
|
-
reason: "stream-json interface not detected on this runner"
|
|
67282
|
-
}
|
|
67662
|
+
resident_session: residentSession
|
|
67283
67663
|
};
|
|
67284
67664
|
}
|
|
67285
67665
|
async function probeClaudeStreamJsonInterface() {
|
|
@@ -67353,29 +67733,28 @@ async function post2(fetchImpl, appUrl2, pathName, body, token, signal) {
|
|
|
67353
67733
|
}
|
|
67354
67734
|
return { status: response.status, json: json2 };
|
|
67355
67735
|
}
|
|
67356
|
-
var
|
|
67736
|
+
var lastCredentialCellBusyNoteAt = 0;
|
|
67357
67737
|
async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime, activeTurnIds, options = {}) {
|
|
67358
|
-
let
|
|
67359
|
-
|
|
67360
|
-
|
|
67361
|
-
|
|
67362
|
-
|
|
67363
|
-
|
|
67364
|
-
|
|
67365
|
-
|
|
67366
|
-
`cannot take the isolated CODEX_HOME lock, so no codex work is being claimed. This is not transient \u2014 it persists until the cause is fixed: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
|
|
67738
|
+
let credentialCellLock = null;
|
|
67739
|
+
try {
|
|
67740
|
+
credentialCellLock = await acquireCodexHomeLock(credentialCellDirForRuntime(runtime, state, config2.homeDir), { waitMs: 0, label: credentialCellLabelForRuntime(runtime) });
|
|
67741
|
+
} catch (error51) {
|
|
67742
|
+
if (Date.now() - lastCredentialCellBusyNoteAt > 5 * 6e4) {
|
|
67743
|
+
lastCredentialCellBusyNoteAt = Date.now();
|
|
67744
|
+
io.stderr.write(
|
|
67745
|
+
`cannot take the isolated credential-cell lock for ${runtime}, so no ${runtime} work is being claimed. This is not transient \u2014 it persists until the cause is fixed: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
|
|
67367
67746
|
`
|
|
67368
|
-
|
|
67369
|
-
}
|
|
67370
|
-
return;
|
|
67747
|
+
);
|
|
67371
67748
|
}
|
|
67372
|
-
|
|
67373
|
-
|
|
67374
|
-
|
|
67375
|
-
|
|
67376
|
-
|
|
67377
|
-
|
|
67749
|
+
return;
|
|
67750
|
+
}
|
|
67751
|
+
if (credentialCellLock === null) {
|
|
67752
|
+
if (Date.now() - lastCredentialCellBusyNoteAt > 5 * 6e4) {
|
|
67753
|
+
lastCredentialCellBusyNoteAt = Date.now();
|
|
67754
|
+
io.stderr.write(`waiting for a sibling ${runtime} turn to release its isolated credential cell before claiming work.
|
|
67755
|
+
`);
|
|
67378
67756
|
}
|
|
67757
|
+
return;
|
|
67379
67758
|
}
|
|
67380
67759
|
try {
|
|
67381
67760
|
const claimStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -67395,7 +67774,7 @@ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime,
|
|
|
67395
67774
|
retry_count: 0,
|
|
67396
67775
|
...claimed.status === 200 ? {} : { error_class: classifyHttpStatus(claimed.status) }
|
|
67397
67776
|
});
|
|
67398
|
-
const
|
|
67777
|
+
const credentialCellLockHeld = true;
|
|
67399
67778
|
const turnExecution = claimed.json.turn_execution;
|
|
67400
67779
|
if (claimed.status === 200 && turnExecution && typeof turnExecution === "object") {
|
|
67401
67780
|
await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, activeTurnIds, {
|
|
@@ -67405,7 +67784,7 @@ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime,
|
|
|
67405
67784
|
startPath: (id) => `/api/runner/turn-executions/${encodeURIComponent(id)}/start`,
|
|
67406
67785
|
resultPath: (id) => `/api/runner/turn-executions/${encodeURIComponent(id)}/result`,
|
|
67407
67786
|
logRef: (id) => `runner-turn:${id}`
|
|
67408
|
-
},
|
|
67787
|
+
}, credentialCellLockHeld);
|
|
67409
67788
|
return;
|
|
67410
67789
|
}
|
|
67411
67790
|
const workOrder = claimed.json.work_order;
|
|
@@ -67419,12 +67798,12 @@ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime,
|
|
|
67419
67798
|
startPath: (id) => `/api/runner/work-orders/${encodeURIComponent(id)}/start`,
|
|
67420
67799
|
resultPath: (id) => `/api/runner/work-orders/${encodeURIComponent(id)}/result`,
|
|
67421
67800
|
logRef: (id) => `runner-local:${id}`
|
|
67422
|
-
},
|
|
67801
|
+
}, credentialCellLockHeld);
|
|
67423
67802
|
} finally {
|
|
67424
|
-
await
|
|
67803
|
+
await credentialCellLock?.release();
|
|
67425
67804
|
}
|
|
67426
67805
|
}
|
|
67427
|
-
async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, activeTurnIds, input,
|
|
67806
|
+
async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, activeTurnIds, input, credentialCellLockHeld) {
|
|
67428
67807
|
const id = input.item.id;
|
|
67429
67808
|
if (typeof id !== "string") {
|
|
67430
67809
|
return;
|
|
@@ -67470,16 +67849,19 @@ async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtim
|
|
|
67470
67849
|
config: config2,
|
|
67471
67850
|
io,
|
|
67472
67851
|
ledger,
|
|
67473
|
-
// DER-2298 (round 1 review, finding 4): `
|
|
67474
|
-
// received from `claimAndExecute`) is only ever true when `claimAndExecute`
|
|
67475
|
-
//
|
|
67476
|
-
// either that SAME configured fallback or `null` (never a
|
|
67477
|
-
// `
|
|
67478
|
-
// that makes claim-time locking complete: a
|
|
67479
|
-
// per-unit surprise). The explicit
|
|
67480
|
-
//
|
|
67481
|
-
// trusting it.
|
|
67482
|
-
|
|
67852
|
+
// DER-2298 (round 1 review, finding 4): `credentialCellLockHeld` (the parameter this
|
|
67853
|
+
// function received from `claimAndExecute`) is only ever true when `claimAndExecute`
|
|
67854
|
+
// acquired the lock for its OWN `runtime` parameter — and `runtimeForClaimedUnit` above
|
|
67855
|
+
// guarantees `turnRuntime` here is either that SAME configured fallback or `null` (never a
|
|
67856
|
+
// different runtime), so whenever `credentialCellLockHeld` is true, `turnRuntime` can only
|
|
67857
|
+
// equal `runtime` too (this is the KEY FACT that makes claim-time locking complete: a
|
|
67858
|
+
// runner's claimed-unit runtime is never a per-unit surprise). The explicit
|
|
67859
|
+
// `turnRuntime === runtime` check is a redundant belt — kept so this stays correct even if
|
|
67860
|
+
// that invariant is ever weakened, rather than silently trusting it.
|
|
67861
|
+
// DER-3022 (round 5 review, P1): generalized from a hardcoded `turnRuntime === "codex"`
|
|
67862
|
+
// comparison — the caller now takes the lock for whichever runtime it is configured with,
|
|
67863
|
+
// claude included, so the comparison must be against THAT runtime, not a literal.
|
|
67864
|
+
...credentialCellLockHeld && turnRuntime === runtime ? { credentialCellLockHeld: true } : {}
|
|
67483
67865
|
},
|
|
67484
67866
|
input.item,
|
|
67485
67867
|
turnRuntime,
|
|
@@ -67921,7 +68303,10 @@ async function prepareForgeTurn(ctx, workOrder, execution, runtime, seams) {
|
|
|
67921
68303
|
// deny is derived from — which must hold on a claude turn too, since a claude turn could
|
|
67922
68304
|
// otherwise plant the legacy path for the next startup migration to pick up. The allow itself is
|
|
67923
68305
|
// gated on modelRuntime inside resolveSandboxSpec; the harness-private base stays denied always.
|
|
67924
|
-
|
|
68306
|
+
// DER-3022: pass the claude isolate too (this turn's own credential cell); resolveSandboxSpec
|
|
68307
|
+
// applies the right absolute-last re-allow based on the actual modelRuntime.
|
|
68308
|
+
codexIsolateHomeDir: stableCodexHomeDir(ctx.state, ctx.config.homeDir),
|
|
68309
|
+
claudeIsolateHomeDir: stableClaudeHomeDir(ctx.state, ctx.config.homeDir)
|
|
67925
68310
|
});
|
|
67926
68311
|
return {
|
|
67927
68312
|
ok: true,
|
|
@@ -67939,7 +68324,7 @@ async function prepareForgeTurn(ctx, workOrder, execution, runtime, seams) {
|
|
|
67939
68324
|
var FORGE_MAX_CHANGED_PATHS = 2e3;
|
|
67940
68325
|
var FORGE_MAX_CHANGED_PATH_LENGTH = 500;
|
|
67941
68326
|
function forgeChangedPathsField(changedPaths) {
|
|
67942
|
-
const withinCaps = changedPaths.length > 0 && changedPaths.length <= FORGE_MAX_CHANGED_PATHS && changedPaths.every((
|
|
68327
|
+
const withinCaps = changedPaths.length > 0 && changedPaths.length <= FORGE_MAX_CHANGED_PATHS && changedPaths.every((path12) => path12.length >= 1 && path12.length <= FORGE_MAX_CHANGED_PATH_LENGTH);
|
|
67943
68328
|
return withinCaps ? { changedPaths: [...changedPaths] } : {};
|
|
67944
68329
|
}
|
|
67945
68330
|
async function finalizeForgeBuildPush(ctx, prep, result, seams) {
|
|
@@ -68071,10 +68456,33 @@ function stableCodexHomeDir(state, homeDir) {
|
|
|
68071
68456
|
const key = createHash4("sha256").update(`codex-home:${state.runner_id ?? "runner"}`).digest("hex").slice(0, 16);
|
|
68072
68457
|
return path10.join(runnerCodexHomeBaseDir(homeDir), `rost-runner-codex-home-${key}`);
|
|
68073
68458
|
}
|
|
68459
|
+
function stableClaudeHomeDir(state, homeDir) {
|
|
68460
|
+
const key = createHash4("sha256").update(`claude-home:${state.runner_id ?? "runner"}`).digest("hex").slice(0, 16);
|
|
68461
|
+
return path10.join(runnerClaudeHomeBaseDir(homeDir), `rost-runner-claude-home-${key}`);
|
|
68462
|
+
}
|
|
68463
|
+
function credentialCellDirForRuntime(runtime, state, homeDir) {
|
|
68464
|
+
return runtime === "codex" ? stableCodexHomeDir(state, homeDir) : stableClaudeHomeDir(state, homeDir);
|
|
68465
|
+
}
|
|
68466
|
+
function credentialCellLabelForRuntime(runtime) {
|
|
68467
|
+
return runtime === "codex" ? "codex home" : "claude credential cell";
|
|
68468
|
+
}
|
|
68469
|
+
async function claudeCellNeedsLogin(cell2) {
|
|
68470
|
+
if (!await isVerifiedExistingCodexHomeDir(cell2)) {
|
|
68471
|
+
return true;
|
|
68472
|
+
}
|
|
68473
|
+
const markerPath = path10.join(path10.dirname(cell2), `${path10.basename(cell2)}.seeded`);
|
|
68474
|
+
const markerInfo = await lstat2(markerPath).catch(() => null);
|
|
68475
|
+
const marked = markerInfo !== null && markerInfo.isFile() && !markerInfo.isSymbolicLink() && markerInfo.nlink === 1 && (typeof process.getuid !== "function" || markerInfo.uid === process.getuid());
|
|
68476
|
+
if (!marked) {
|
|
68477
|
+
return true;
|
|
68478
|
+
}
|
|
68479
|
+
return !await hasUsableClaudeCredentialFile(cell2);
|
|
68480
|
+
}
|
|
68074
68481
|
function resolveRuntimeSourceHomes() {
|
|
68075
68482
|
return {
|
|
68076
68483
|
claudeSourceHome: resolveSourceClaudeConfigDir(process.env),
|
|
68077
|
-
codexSourceHome: resolveSourceCodexHomeDir(process.env)
|
|
68484
|
+
codexSourceHome: resolveSourceCodexHomeDir(process.env),
|
|
68485
|
+
claudeSecureStorageSourceHome: resolveSourceClaudeSecureStorageDir(process.env)
|
|
68078
68486
|
};
|
|
68079
68487
|
}
|
|
68080
68488
|
async function sweepOrphanedTurnTempFiles(excludeNames) {
|
|
@@ -68101,10 +68509,10 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind, resumeSessionIdOve
|
|
|
68101
68509
|
return { ok: false, summary: refusal };
|
|
68102
68510
|
}
|
|
68103
68511
|
}
|
|
68104
|
-
const
|
|
68105
|
-
const
|
|
68106
|
-
if (
|
|
68107
|
-
return { ok: false, summary:
|
|
68512
|
+
const credentialCellLockWaitMs = kind === "work_order" ? 30 * 6e4 : 6e4;
|
|
68513
|
+
const credentialCellLock = ctx.credentialCellLockHeld !== true ? await acquireCodexHomeLock(credentialCellDirForRuntime(runtime, ctx.state, ctx.config.homeDir), { waitMs: credentialCellLockWaitMs, label: credentialCellLabelForRuntime(runtime) }) : null;
|
|
68514
|
+
if (ctx.credentialCellLockHeld !== true && credentialCellLock === null) {
|
|
68515
|
+
return { ok: false, summary: credentialCellBusySummary(credentialCellLockWaitMs, runtime) };
|
|
68108
68516
|
}
|
|
68109
68517
|
try {
|
|
68110
68518
|
let forgePrep = null;
|
|
@@ -68137,7 +68545,10 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind, resumeSessionIdOve
|
|
|
68137
68545
|
modelRuntime: runtime,
|
|
68138
68546
|
...runtimeSourceHomes,
|
|
68139
68547
|
// DER-1485 (round 6): see above — allow is codex-only, the derived legacy deny is universal.
|
|
68140
|
-
|
|
68548
|
+
// DER-3022: also pass this turn's claude credential cell so a claude runtime gets its own
|
|
68549
|
+
// absolute-last re-allow; the right isolate is selected inside resolveSandboxSpec.
|
|
68550
|
+
codexIsolateHomeDir: stableCodexHomeDir(ctx.state, ctx.config.homeDir),
|
|
68551
|
+
claudeIsolateHomeDir: stableClaudeHomeDir(ctx.state, ctx.config.homeDir)
|
|
68141
68552
|
});
|
|
68142
68553
|
}
|
|
68143
68554
|
const repoDir = forgePrep ? forgePrep.repoDir : null;
|
|
@@ -68148,12 +68559,17 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind, resumeSessionIdOve
|
|
|
68148
68559
|
const mcpConfig = JSON.stringify({ mcpServers: { rost: { type: "http", url: url2, headers: { Authorization: `Bearer ${token}` } } } });
|
|
68149
68560
|
const configPath = path10.join(tmpdir2(), `rost-runner-mcp-${createHash4("sha256").update(`${url2}:${Date.now()}:${Math.random()}`).digest("hex").slice(0, 16)}.json`);
|
|
68150
68561
|
const prompt = buildTurnPrompt({ kind, workOrder, execution, hasWorkspace: forgePrep !== null });
|
|
68151
|
-
const
|
|
68152
|
-
|
|
68562
|
+
const claudeCell = runtime === "claude" ? stableClaudeHomeDir(ctx.state, ctx.config.homeDir) : null;
|
|
68563
|
+
const claudeConfigDir = runtime === "claude" && claudeCell !== null ? await provisionIsolatedClaudeConfigDir(
|
|
68153
68564
|
resumeClaudeConfigDir ?? path10.join(tmpdir2(), `rost-runner-claude-config-${createHash4("sha256").update(`${configPath}:cfg`).digest("hex").slice(0, 16)}`),
|
|
68565
|
+
claudeCell,
|
|
68154
68566
|
(message) => ctx.io.stderr.write(`${redactForLog(message)}
|
|
68155
68567
|
`)
|
|
68156
68568
|
) : null;
|
|
68569
|
+
if (runtime === "claude" && claudeCell !== null && await claudeCellNeedsLogin(claudeCell)) {
|
|
68570
|
+
ctx.io.stderr.write(`${redactForLog(`no dedicated runner credential found \u2014 claude turns will fail authentication; run \`${cliBrand.binName} runner login\` to provision one`)}
|
|
68571
|
+
`);
|
|
68572
|
+
}
|
|
68157
68573
|
const codexHomeDir = runtime === "codex" ? await provisionIsolatedCodexHomeDir(process.env, stableCodexHomeDir(ctx.state, ctx.config.homeDir), repoDir ?? tmpdir2(), (message) => ctx.io.stderr.write(`${redactForLog(message)}
|
|
68158
68574
|
`)) : null;
|
|
68159
68575
|
await writeFile6(configPath, `${mcpConfig}
|
|
@@ -68213,7 +68629,7 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind, resumeSessionIdOve
|
|
|
68213
68629
|
// A Forge turn's cwd is its worktree; every other turn stays in tmpdir.
|
|
68214
68630
|
cwd: plan.repoDir ?? tmpdir2(),
|
|
68215
68631
|
timeoutMs: plan.timeoutMs,
|
|
68216
|
-
env: buildModelSpawnEnv(process.env, claudeConfigDir ?? void 0, codexHomeDir ?? void 0),
|
|
68632
|
+
env: buildModelSpawnEnv(process.env, claudeConfigDir ?? void 0, codexHomeDir ?? void 0, runtime === "claude" ? claudeCell ?? void 0 : void 0),
|
|
68217
68633
|
expectSessionEnvelope,
|
|
68218
68634
|
resumeSessionId,
|
|
68219
68635
|
// T1.6: only Forge turns carry a structured evidence contract worth parsing.
|
|
@@ -68259,7 +68675,7 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind, resumeSessionIdOve
|
|
|
68259
68675
|
}
|
|
68260
68676
|
return result;
|
|
68261
68677
|
} finally {
|
|
68262
|
-
await
|
|
68678
|
+
await credentialCellLock?.release();
|
|
68263
68679
|
}
|
|
68264
68680
|
}
|
|
68265
68681
|
var MERGE_MERGEABILITY_POLLS = 5;
|
|
@@ -68374,7 +68790,10 @@ async function resolveMergeConflictTurn(ctx, input) {
|
|
|
68374
68790
|
// Resolved once at the top of this function (DER-2842) — before the workspace exists.
|
|
68375
68791
|
...runtimeSourceHomes,
|
|
68376
68792
|
// DER-1485 (round 6): see above — allow is codex-only, the derived legacy deny is universal.
|
|
68377
|
-
|
|
68793
|
+
// DER-3022: pass the claude cell too so the seatbelt can re-allow this turn's own claude
|
|
68794
|
+
// credential cell when the merge-conflict runtime is claude.
|
|
68795
|
+
codexIsolateHomeDir: stableCodexHomeDir(ctx.state, ctx.config.homeDir),
|
|
68796
|
+
claudeIsolateHomeDir: stableClaudeHomeDir(ctx.state, ctx.config.homeDir)
|
|
68378
68797
|
});
|
|
68379
68798
|
const sandboxRefusal = sandboxRequiredTurnRefusal({ mode: ctx.config.sandboxMode, platform: process.platform, spec: sandbox });
|
|
68380
68799
|
if (sandboxRefusal) {
|
|
@@ -68408,18 +68827,23 @@ async function spawnMergeConflictModel(ctx, input) {
|
|
|
68408
68827
|
const url2 = typeof mcp.url === "string" ? `${ctx.appUrl.replace(/\/+$/, "")}${mcp.url}` : `${ctx.appUrl.replace(/\/+$/, "")}/mcp`;
|
|
68409
68828
|
const mcpConfig = JSON.stringify({ mcpServers: { rost: { type: "http", url: url2, headers: { Authorization: `Bearer ${token}` } } } });
|
|
68410
68829
|
const configPath = path10.join(tmpdir2(), `rost-runner-mcp-${createHash4("sha256").update(`${url2}:${Date.now()}:${Math.random()}`).digest("hex").slice(0, 16)}.json`);
|
|
68411
|
-
const
|
|
68412
|
-
const
|
|
68413
|
-
if (
|
|
68414
|
-
return { ok: false, summary:
|
|
68830
|
+
const credentialCellLockWaitMs = 30 * 6e4;
|
|
68831
|
+
const credentialCellLock = ctx.credentialCellLockHeld !== true ? await acquireCodexHomeLock(credentialCellDirForRuntime(input.runtime, ctx.state, ctx.config.homeDir), { waitMs: credentialCellLockWaitMs, label: credentialCellLabelForRuntime(input.runtime) }) : null;
|
|
68832
|
+
if (ctx.credentialCellLockHeld !== true && credentialCellLock === null) {
|
|
68833
|
+
return { ok: false, summary: credentialCellBusySummary(credentialCellLockWaitMs, input.runtime) };
|
|
68415
68834
|
}
|
|
68416
68835
|
try {
|
|
68417
|
-
const
|
|
68418
|
-
|
|
68836
|
+
const claudeCell = input.runtime === "claude" ? stableClaudeHomeDir(ctx.state, ctx.config.homeDir) : null;
|
|
68837
|
+
const claudeConfigDir = input.runtime === "claude" && claudeCell !== null ? await provisionIsolatedClaudeConfigDir(
|
|
68419
68838
|
path10.join(tmpdir2(), `rost-runner-claude-config-${createHash4("sha256").update(`${configPath}:cfg`).digest("hex").slice(0, 16)}`),
|
|
68839
|
+
claudeCell,
|
|
68420
68840
|
(message) => ctx.io.stderr.write(`${redactForLog(message)}
|
|
68421
68841
|
`)
|
|
68422
68842
|
) : null;
|
|
68843
|
+
if (input.runtime === "claude" && claudeCell !== null && await claudeCellNeedsLogin(claudeCell)) {
|
|
68844
|
+
ctx.io.stderr.write(`${redactForLog(`no dedicated runner credential found \u2014 claude turns will fail authentication; run \`${cliBrand.binName} runner login\` to provision one`)}
|
|
68845
|
+
`);
|
|
68846
|
+
}
|
|
68423
68847
|
const codexHomeDir = input.runtime === "codex" ? await provisionIsolatedCodexHomeDir(process.env, stableCodexHomeDir(ctx.state, ctx.config.homeDir), input.workspace.worktreeDir, (message) => ctx.io.stderr.write(`${redactForLog(message)}
|
|
68424
68848
|
`)) : null;
|
|
68425
68849
|
await writeFile6(configPath, `${mcpConfig}
|
|
@@ -68454,7 +68878,7 @@ async function spawnMergeConflictModel(ctx, input) {
|
|
|
68454
68878
|
cwd: input.workspace.worktreeDir,
|
|
68455
68879
|
timeoutMs: plan.timeoutMs,
|
|
68456
68880
|
// The model gets the EXPLICIT allowlist env — never the merge token, GIT_ASKPASS, or the bearer.
|
|
68457
|
-
env: buildModelSpawnEnv(process.env, claudeConfigDir ?? void 0, codexHomeDir ?? void 0),
|
|
68881
|
+
env: buildModelSpawnEnv(process.env, claudeConfigDir ?? void 0, codexHomeDir ?? void 0, input.runtime === "claude" ? claudeCell ?? void 0 : void 0),
|
|
68458
68882
|
expectSessionEnvelope: false
|
|
68459
68883
|
});
|
|
68460
68884
|
const exitClass = classifyModelExit(result);
|
|
@@ -68476,7 +68900,7 @@ async function spawnMergeConflictModel(ctx, input) {
|
|
|
68476
68900
|
void sweepOrphanedTurnTempFiles(journaledClaudeConfigDirNames(ctx.state));
|
|
68477
68901
|
return result;
|
|
68478
68902
|
} finally {
|
|
68479
|
-
await
|
|
68903
|
+
await credentialCellLock?.release();
|
|
68480
68904
|
}
|
|
68481
68905
|
}
|
|
68482
68906
|
async function runForgeMergeTurn(ctx, workOrder, execution, runtime) {
|
|
@@ -68902,14 +69326,19 @@ async function settleInFlightWorkOrder(stateFile, state, workOrderId) {
|
|
|
68902
69326
|
}
|
|
68903
69327
|
}
|
|
68904
69328
|
async function reconcileOrphanedInFlightWorkOrders(deps) {
|
|
68905
|
-
const { fetchImpl, appUrl: appUrl2, state, config: config2, io, allowResume } = deps;
|
|
69329
|
+
const { fetchImpl, appUrl: appUrl2, state, config: config2, io, allowResume, localRuntime, onlyWorkOrderIds } = deps;
|
|
68906
69330
|
const journal = state.in_flight_work_orders;
|
|
68907
69331
|
if (!journal || Object.keys(journal).length === 0) {
|
|
68908
|
-
return;
|
|
69332
|
+
return { deferredWorkOrderIds: [] };
|
|
68909
69333
|
}
|
|
68910
|
-
|
|
69334
|
+
const entries = Object.entries(journal).filter(([id]) => onlyWorkOrderIds === null || onlyWorkOrderIds === void 0 || onlyWorkOrderIds.has(id));
|
|
69335
|
+
if (entries.length === 0) {
|
|
69336
|
+
return { deferredWorkOrderIds: [] };
|
|
69337
|
+
}
|
|
69338
|
+
io.stdout.write(`reconciling ${entries.length} orphaned work order(s)
|
|
68911
69339
|
`);
|
|
68912
|
-
|
|
69340
|
+
const deferredWorkOrderIds = /* @__PURE__ */ new Set();
|
|
69341
|
+
for (const [id, entry] of entries) {
|
|
68913
69342
|
try {
|
|
68914
69343
|
let verdict = { kind: "missing" };
|
|
68915
69344
|
if (entry.runtime === "claude" && entry.claude_session_id && entry.claude_config_dir) {
|
|
@@ -68925,6 +69354,13 @@ async function reconcileOrphanedInFlightWorkOrders(deps) {
|
|
|
68925
69354
|
}
|
|
68926
69355
|
const profile = parseRunnerExecutionContract(entry.item ?? {})?.profile ?? null;
|
|
68927
69356
|
let action = decideReconcileAction({ entry, verdict, profile, now: /* @__PURE__ */ new Date() });
|
|
69357
|
+
const needsExecutionRuntime = action.type === "resume" || action.type === "reverify_merge";
|
|
69358
|
+
if (allowResume && needsExecutionRuntime && localRuntime !== entry.runtime) {
|
|
69359
|
+
deferredWorkOrderIds.add(id);
|
|
69360
|
+
io.stderr.write(`work_order_id=${id} ${entry.runtime} runtime is not locally claimable; keeping orphan journal until the selected runtime contract is proven
|
|
69361
|
+
`);
|
|
69362
|
+
continue;
|
|
69363
|
+
}
|
|
68928
69364
|
if (action.type === "resume" && !allowResume) {
|
|
68929
69365
|
action = {
|
|
68930
69366
|
type: "report_failed",
|
|
@@ -68936,17 +69372,17 @@ async function reconcileOrphanedInFlightWorkOrders(deps) {
|
|
|
68936
69372
|
continue;
|
|
68937
69373
|
}
|
|
68938
69374
|
if (action.type === "report_succeeded" || action.type === "report_failed") {
|
|
68939
|
-
const
|
|
69375
|
+
const reportBody = {
|
|
68940
69376
|
status: action.type === "report_succeeded" ? "succeeded" : "failed",
|
|
68941
69377
|
summary: capResultSummary(scrubTranscriptSessionIds(redactForLog(action.summary))),
|
|
68942
69378
|
log_ref: `runner-local:${id}`,
|
|
68943
69379
|
model: entry.runtime === "codex" ? "codex-cli" : "claude-cli"
|
|
68944
69380
|
};
|
|
68945
|
-
const { outcome
|
|
69381
|
+
const { outcome, lastStatus } = await postResultThenSettle({
|
|
68946
69382
|
fetchImpl,
|
|
68947
69383
|
appUrl: appUrl2,
|
|
68948
69384
|
resultPath: `/api/runner/work-orders/${encodeURIComponent(id)}/result`,
|
|
68949
|
-
reportBody
|
|
69385
|
+
reportBody,
|
|
68950
69386
|
runnerSecret: state.runner_secret,
|
|
68951
69387
|
state,
|
|
68952
69388
|
stateFile: config2.stateFile,
|
|
@@ -68954,57 +69390,63 @@ async function reconcileOrphanedInFlightWorkOrders(deps) {
|
|
|
68954
69390
|
kind: "work_order",
|
|
68955
69391
|
idField: "work_order_id",
|
|
68956
69392
|
id,
|
|
68957
|
-
model:
|
|
69393
|
+
model: reportBody.model,
|
|
68958
69394
|
originalOk: action.type === "report_succeeded",
|
|
68959
69395
|
note: "reconciled after runner restart"
|
|
68960
69396
|
});
|
|
68961
|
-
if (
|
|
68962
|
-
io.stderr.write(`work_order_id=${id} reconcile report failed ${
|
|
69397
|
+
if (outcome === "unresolved") {
|
|
69398
|
+
io.stderr.write(`work_order_id=${id} reconcile report failed ${lastStatus}; keeping journal entry for next restart
|
|
68963
69399
|
`);
|
|
68964
69400
|
}
|
|
68965
69401
|
continue;
|
|
68966
69402
|
}
|
|
68967
69403
|
if (action.type === "reverify_merge") {
|
|
68968
|
-
const
|
|
68969
|
-
if (
|
|
68970
|
-
io.stderr.write(`work_order_id=${id}
|
|
69404
|
+
const credentialCellLock2 = await acquireCodexHomeLock(credentialCellDirForRuntime(entry.runtime, state, config2.homeDir), { waitMs: 0, label: credentialCellLabelForRuntime(entry.runtime) });
|
|
69405
|
+
if (credentialCellLock2 === null) {
|
|
69406
|
+
io.stderr.write(`work_order_id=${id} credential cell busy; keeping merge reverify journal for the next restart
|
|
68971
69407
|
`);
|
|
68972
69408
|
continue;
|
|
68973
69409
|
}
|
|
68974
69410
|
try {
|
|
68975
|
-
|
|
69411
|
+
if (entry.runtime === "claude" && await claudeCellNeedsLogin(stableClaudeHomeDir(state, config2.homeDir))) {
|
|
69412
|
+
deferredWorkOrderIds.add(id);
|
|
69413
|
+
io.stderr.write(`work_order_id=${id} Claude auth proof is unavailable; keeping merge reverify journal (run \`${cliBrand.binName} runner login\`)
|
|
69414
|
+
`);
|
|
69415
|
+
continue;
|
|
69416
|
+
}
|
|
69417
|
+
const ctx = {
|
|
68976
69418
|
fetchImpl,
|
|
68977
69419
|
appUrl: appUrl2,
|
|
68978
69420
|
state,
|
|
68979
69421
|
config: config2,
|
|
68980
69422
|
io,
|
|
68981
69423
|
ledger: new StepLedger(),
|
|
68982
|
-
|
|
69424
|
+
credentialCellLockHeld: true
|
|
68983
69425
|
};
|
|
68984
|
-
const
|
|
68985
|
-
const
|
|
68986
|
-
status:
|
|
68987
|
-
summary: capResultSummary(
|
|
69426
|
+
const result = await spawnRunnerTurn(ctx, entry.item ?? {}, entry.runtime, "work_order");
|
|
69427
|
+
const reportBody = {
|
|
69428
|
+
status: result.ok ? "succeeded" : "failed",
|
|
69429
|
+
summary: capResultSummary(result.summary),
|
|
68988
69430
|
log_ref: `runner-local:${id}`,
|
|
68989
69431
|
model: entry.runtime === "codex" ? "codex-cli" : "claude-cli",
|
|
68990
|
-
step_ledger:
|
|
68991
|
-
...
|
|
68992
|
-
...
|
|
68993
|
-
...
|
|
68994
|
-
...
|
|
68995
|
-
...
|
|
68996
|
-
...
|
|
68997
|
-
...
|
|
68998
|
-
...
|
|
68999
|
-
...
|
|
69000
|
-
...
|
|
69001
|
-
...
|
|
69432
|
+
step_ledger: ctx.ledger.entries(),
|
|
69433
|
+
...result.transcript ? { transcript: result.transcript } : {},
|
|
69434
|
+
...result.inputTokens != null ? { input_tokens: result.inputTokens } : {},
|
|
69435
|
+
...result.outputTokens != null ? { output_tokens: result.outputTokens } : {},
|
|
69436
|
+
...result.prUrl ? { pr_url: result.prUrl } : {},
|
|
69437
|
+
...result.branch ? { branch: result.branch } : {},
|
|
69438
|
+
...result.commitSha ? { commit_sha: result.commitSha } : {},
|
|
69439
|
+
...result.evidence && result.evidence.length > 0 ? { evidence: result.evidence } : {},
|
|
69440
|
+
...result.changedPaths && result.changedPaths.length > 0 ? { changed_paths: result.changedPaths } : {},
|
|
69441
|
+
...result.ok && result.merged === true ? { merged: true } : {},
|
|
69442
|
+
...result.ok && result.merged === true && result.mergedCommitSha ? { merged_commit_sha: result.mergedCommitSha } : {},
|
|
69443
|
+
...result.planArtifact ? { plan_artifact: result.planArtifact } : {}
|
|
69002
69444
|
};
|
|
69003
|
-
const { outcome
|
|
69445
|
+
const { outcome, lastStatus } = await postResultThenSettle({
|
|
69004
69446
|
fetchImpl,
|
|
69005
69447
|
appUrl: appUrl2,
|
|
69006
69448
|
resultPath: `/api/runner/work-orders/${encodeURIComponent(id)}/result`,
|
|
69007
|
-
reportBody
|
|
69449
|
+
reportBody,
|
|
69008
69450
|
runnerSecret: state.runner_secret,
|
|
69009
69451
|
state,
|
|
69010
69452
|
stateFile: config2.stateFile,
|
|
@@ -69012,78 +69454,558 @@ async function reconcileOrphanedInFlightWorkOrders(deps) {
|
|
|
69012
69454
|
kind: "work_order",
|
|
69013
69455
|
idField: "work_order_id",
|
|
69014
69456
|
id,
|
|
69015
|
-
model:
|
|
69016
|
-
originalOk:
|
|
69457
|
+
model: reportBody.model,
|
|
69458
|
+
originalOk: result.ok,
|
|
69017
69459
|
note: "reconciled after runner restart"
|
|
69018
69460
|
});
|
|
69019
|
-
if (
|
|
69020
|
-
io.stderr.write(`work_order_id=${id} merge reverify report failed ${
|
|
69461
|
+
if (outcome === "unresolved") {
|
|
69462
|
+
io.stderr.write(`work_order_id=${id} merge reverify report failed ${lastStatus}; keeping journal entry for next restart
|
|
69021
69463
|
`);
|
|
69022
69464
|
}
|
|
69023
69465
|
} finally {
|
|
69024
|
-
await
|
|
69466
|
+
await credentialCellLock2?.release();
|
|
69025
69467
|
}
|
|
69026
69468
|
continue;
|
|
69027
69469
|
}
|
|
69028
|
-
|
|
69029
|
-
|
|
69030
|
-
|
|
69031
|
-
|
|
69032
|
-
|
|
69033
|
-
|
|
69034
|
-
|
|
69035
|
-
|
|
69036
|
-
|
|
69037
|
-
|
|
69038
|
-
|
|
69039
|
-
|
|
69040
|
-
|
|
69041
|
-
|
|
69042
|
-
|
|
69043
|
-
|
|
69044
|
-
|
|
69045
|
-
|
|
69046
|
-
|
|
69047
|
-
|
|
69048
|
-
|
|
69049
|
-
|
|
69050
|
-
|
|
69051
|
-
|
|
69052
|
-
|
|
69053
|
-
|
|
69054
|
-
|
|
69055
|
-
|
|
69056
|
-
|
|
69057
|
-
|
|
69058
|
-
|
|
69059
|
-
|
|
69060
|
-
|
|
69061
|
-
|
|
69062
|
-
|
|
69063
|
-
|
|
69064
|
-
|
|
69065
|
-
|
|
69066
|
-
|
|
69067
|
-
|
|
69068
|
-
|
|
69069
|
-
|
|
69070
|
-
|
|
69071
|
-
|
|
69072
|
-
|
|
69073
|
-
|
|
69074
|
-
|
|
69470
|
+
let credentialCellLock;
|
|
69471
|
+
try {
|
|
69472
|
+
credentialCellLock = await acquireCodexHomeLock(
|
|
69473
|
+
stableClaudeHomeDir(state, config2.homeDir),
|
|
69474
|
+
{ waitMs: 0, label: "claude credential cell" }
|
|
69475
|
+
);
|
|
69476
|
+
} catch {
|
|
69477
|
+
deferredWorkOrderIds.add(id);
|
|
69478
|
+
io.stderr.write(`work_order_id=${id} claude credential cell unavailable; skipping resume this pass (retried on the next reconcile)
|
|
69479
|
+
`);
|
|
69480
|
+
continue;
|
|
69481
|
+
}
|
|
69482
|
+
if (credentialCellLock === null) {
|
|
69483
|
+
deferredWorkOrderIds.add(id);
|
|
69484
|
+
io.stderr.write(`work_order_id=${id} claude credential cell busy; skipping resume this pass (retried on the next reconcile)
|
|
69485
|
+
`);
|
|
69486
|
+
continue;
|
|
69487
|
+
}
|
|
69488
|
+
try {
|
|
69489
|
+
if (await claudeCellNeedsLogin(stableClaudeHomeDir(state, config2.homeDir))) {
|
|
69490
|
+
deferredWorkOrderIds.add(id);
|
|
69491
|
+
io.stderr.write(`work_order_id=${id} Claude auth proof is unavailable; keeping resume journal (run \`${cliBrand.binName} runner login\`)
|
|
69492
|
+
`);
|
|
69493
|
+
continue;
|
|
69494
|
+
}
|
|
69495
|
+
const ctx = {
|
|
69496
|
+
fetchImpl,
|
|
69497
|
+
appUrl: appUrl2,
|
|
69498
|
+
state,
|
|
69499
|
+
config: config2,
|
|
69500
|
+
io,
|
|
69501
|
+
ledger: new StepLedger(),
|
|
69502
|
+
credentialCellLockHeld: true
|
|
69503
|
+
};
|
|
69504
|
+
const result = await spawnRunnerTurn(ctx, entry.item ?? {}, "claude", "work_order", entry.claude_session_id, entry.claude_config_dir);
|
|
69505
|
+
const reportBody = {
|
|
69506
|
+
status: result.ok ? "succeeded" : "failed",
|
|
69507
|
+
summary: capResultSummary(result.summary),
|
|
69508
|
+
log_ref: `runner-local:${id}`,
|
|
69509
|
+
model: "claude-cli",
|
|
69510
|
+
// DER-1331 (T1.9): mirror the normal executeClaimedUnit work-order report — ship the
|
|
69511
|
+
// harness step ledger and the model transcript when the resume turn produced one.
|
|
69512
|
+
step_ledger: ctx.ledger.entries(),
|
|
69513
|
+
...result.transcript ? { transcript: result.transcript } : {},
|
|
69514
|
+
...result.inputTokens != null ? { input_tokens: result.inputTokens } : {},
|
|
69515
|
+
...result.outputTokens != null ? { output_tokens: result.outputTokens } : {},
|
|
69516
|
+
...result.prUrl ? { pr_url: result.prUrl } : {},
|
|
69517
|
+
...result.branch ? { branch: result.branch } : {},
|
|
69518
|
+
...result.commitSha ? { commit_sha: result.commitSha } : {},
|
|
69519
|
+
...result.evidence && result.evidence.length > 0 ? { evidence: result.evidence } : {},
|
|
69520
|
+
...result.changedPaths && result.changedPaths.length > 0 ? { changed_paths: result.changedPaths } : {},
|
|
69521
|
+
...result.ok && result.merged === true ? { merged: true } : {},
|
|
69522
|
+
...result.ok && result.merged === true && result.mergedCommitSha ? { merged_commit_sha: result.mergedCommitSha } : {},
|
|
69523
|
+
...result.planArtifact ? { plan_artifact: result.planArtifact } : {}
|
|
69524
|
+
};
|
|
69525
|
+
const { outcome, lastStatus } = await postResultThenSettle({
|
|
69526
|
+
fetchImpl,
|
|
69527
|
+
appUrl: appUrl2,
|
|
69528
|
+
resultPath: `/api/runner/work-orders/${encodeURIComponent(id)}/result`,
|
|
69529
|
+
reportBody,
|
|
69530
|
+
runnerSecret: state.runner_secret,
|
|
69531
|
+
state,
|
|
69532
|
+
stateFile: config2.stateFile,
|
|
69533
|
+
io,
|
|
69534
|
+
kind: "work_order",
|
|
69535
|
+
idField: "work_order_id",
|
|
69536
|
+
id,
|
|
69537
|
+
model: "claude-cli",
|
|
69538
|
+
originalOk: result.ok,
|
|
69539
|
+
note: "reconciled after runner restart"
|
|
69540
|
+
});
|
|
69541
|
+
if (outcome === "unresolved") {
|
|
69542
|
+
io.stderr.write(`work_order_id=${id} reconcile report failed ${lastStatus}; keeping journal entry for next restart
|
|
69075
69543
|
`);
|
|
69544
|
+
}
|
|
69545
|
+
} finally {
|
|
69546
|
+
await credentialCellLock.release();
|
|
69076
69547
|
}
|
|
69077
69548
|
} catch (error51) {
|
|
69078
69549
|
io.stderr.write(`reconcile work_order_id=${id} error (continuing): ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
|
|
69079
69550
|
`);
|
|
69080
69551
|
}
|
|
69081
69552
|
}
|
|
69553
|
+
return { deferredWorkOrderIds: [...deferredWorkOrderIds] };
|
|
69082
69554
|
}
|
|
69083
69555
|
function usage() {
|
|
69084
69556
|
return "Usage: rost runner serve [--name <text>] [--state-file <path>] [--heartbeat-ms <n>] [--interactive-claim-ms <n>] [--user-code <code>] [--runtime auto|claude|codex] [--max-sessions <n>] [--once] [--execute]";
|
|
69085
69557
|
}
|
|
69086
69558
|
|
|
69559
|
+
// src/runner-login.ts
|
|
69560
|
+
var LOGIN_ENV_ALLOWLIST = [
|
|
69561
|
+
"PATH",
|
|
69562
|
+
"HOME",
|
|
69563
|
+
"USER",
|
|
69564
|
+
"LOGNAME",
|
|
69565
|
+
"LANG",
|
|
69566
|
+
"LC_ALL",
|
|
69567
|
+
"LC_CTYPE",
|
|
69568
|
+
"TERM",
|
|
69569
|
+
"COLORTERM",
|
|
69570
|
+
"TMPDIR",
|
|
69571
|
+
"SHELL",
|
|
69572
|
+
"__CF_USER_TEXT_ENCODING",
|
|
69573
|
+
"HTTP_PROXY",
|
|
69574
|
+
"HTTPS_PROXY",
|
|
69575
|
+
"NO_PROXY",
|
|
69576
|
+
"http_proxy",
|
|
69577
|
+
"https_proxy",
|
|
69578
|
+
"no_proxy"
|
|
69579
|
+
];
|
|
69580
|
+
function buildLoginEnv(base, configDir, secureStorageDir = configDir) {
|
|
69581
|
+
const env = {};
|
|
69582
|
+
for (const key of LOGIN_ENV_ALLOWLIST) {
|
|
69583
|
+
const value = base[key];
|
|
69584
|
+
if (typeof value === "string") {
|
|
69585
|
+
env[key] = value;
|
|
69586
|
+
}
|
|
69587
|
+
}
|
|
69588
|
+
env.CLAUDE_CONFIG_DIR = configDir;
|
|
69589
|
+
env.CLAUDE_SECURESTORAGE_CONFIG_DIR = secureStorageDir;
|
|
69590
|
+
return env;
|
|
69591
|
+
}
|
|
69592
|
+
function buildOperatorIdentityEnv(base, explicitConfigDir, secureStorageDir) {
|
|
69593
|
+
const env = {};
|
|
69594
|
+
for (const key of LOGIN_ENV_ALLOWLIST) {
|
|
69595
|
+
const value = base[key];
|
|
69596
|
+
if (typeof value === "string") {
|
|
69597
|
+
env[key] = value;
|
|
69598
|
+
}
|
|
69599
|
+
}
|
|
69600
|
+
if (explicitConfigDir !== null) {
|
|
69601
|
+
env.CLAUDE_CONFIG_DIR = explicitConfigDir;
|
|
69602
|
+
}
|
|
69603
|
+
env.CLAUDE_SECURESTORAGE_CONFIG_DIR = secureStorageDir;
|
|
69604
|
+
return env;
|
|
69605
|
+
}
|
|
69606
|
+
async function spawnLogin(cmd, args, opts) {
|
|
69607
|
+
return new Promise((resolve2, reject) => {
|
|
69608
|
+
const child = spawn2(cmd, args, { env: opts.env, stdio: "inherit" });
|
|
69609
|
+
child.on("error", (error51) => {
|
|
69610
|
+
reject(error51);
|
|
69611
|
+
});
|
|
69612
|
+
child.on("close", (code) => {
|
|
69613
|
+
resolve2(code ?? 1);
|
|
69614
|
+
});
|
|
69615
|
+
});
|
|
69616
|
+
}
|
|
69617
|
+
async function spawnStatus(cmd, args, opts) {
|
|
69618
|
+
return new Promise((resolve2, reject) => {
|
|
69619
|
+
const chunks = [];
|
|
69620
|
+
const child = spawn2(cmd, args, {
|
|
69621
|
+
env: opts.env,
|
|
69622
|
+
stdio: ["ignore", "pipe", "inherit"]
|
|
69623
|
+
});
|
|
69624
|
+
child.stdout?.on("data", (chunk) => {
|
|
69625
|
+
chunks.push(chunk);
|
|
69626
|
+
});
|
|
69627
|
+
child.on("error", (error51) => {
|
|
69628
|
+
reject(error51);
|
|
69629
|
+
});
|
|
69630
|
+
child.on("close", (code) => {
|
|
69631
|
+
resolve2({ code: code ?? 1, stdout: Buffer.concat(chunks).toString("utf8") });
|
|
69632
|
+
});
|
|
69633
|
+
});
|
|
69634
|
+
}
|
|
69635
|
+
var LOGIN_CREDENTIAL_CELL_LOCK_WAIT_MS = 5e3;
|
|
69636
|
+
function parseClaudeAccountIdentity(status) {
|
|
69637
|
+
if (status.code !== 0) {
|
|
69638
|
+
return null;
|
|
69639
|
+
}
|
|
69640
|
+
try {
|
|
69641
|
+
const parsed = JSON.parse(status.stdout);
|
|
69642
|
+
if (parsed.loggedIn !== true || typeof parsed.email !== "string" || parsed.email.length === 0) {
|
|
69643
|
+
return null;
|
|
69644
|
+
}
|
|
69645
|
+
return { email: parsed.email, orgId: typeof parsed.orgId === "string" ? parsed.orgId : null };
|
|
69646
|
+
} catch {
|
|
69647
|
+
return null;
|
|
69648
|
+
}
|
|
69649
|
+
}
|
|
69650
|
+
function isSameClaudeAccount(a, b) {
|
|
69651
|
+
return a.email.toLowerCase() === b.email.toLowerCase();
|
|
69652
|
+
}
|
|
69653
|
+
function evaluateIdentityCollision(cellAccount, operatorAccount) {
|
|
69654
|
+
if (operatorAccount === null) {
|
|
69655
|
+
return { kind: "no_operator_login" };
|
|
69656
|
+
}
|
|
69657
|
+
if (cellAccount === null) {
|
|
69658
|
+
return { kind: "cell_identity_unreadable" };
|
|
69659
|
+
}
|
|
69660
|
+
if (isSameClaudeAccount(cellAccount, operatorAccount)) {
|
|
69661
|
+
return { kind: "collision" };
|
|
69662
|
+
}
|
|
69663
|
+
return { kind: "distinct" };
|
|
69664
|
+
}
|
|
69665
|
+
async function isTrustworthyAutoDetectStateFile(filePath) {
|
|
69666
|
+
const info = await lstat3(filePath).catch(() => null);
|
|
69667
|
+
if (info === null) {
|
|
69668
|
+
return true;
|
|
69669
|
+
}
|
|
69670
|
+
if (info.isSymbolicLink() || !info.isFile()) {
|
|
69671
|
+
return false;
|
|
69672
|
+
}
|
|
69673
|
+
if (typeof process.getuid === "function" && info.uid !== process.getuid()) {
|
|
69674
|
+
return false;
|
|
69675
|
+
}
|
|
69676
|
+
return (info.mode & 18) === 0;
|
|
69677
|
+
}
|
|
69678
|
+
async function canonicalPathIfExisting(value, sourceName) {
|
|
69679
|
+
const absolute = path11.resolve(value);
|
|
69680
|
+
const missing = [];
|
|
69681
|
+
let current = absolute;
|
|
69682
|
+
for (; ; ) {
|
|
69683
|
+
const info = await lstat3(current).catch(() => null);
|
|
69684
|
+
if (info !== null) {
|
|
69685
|
+
if (sourceName && info.isSymbolicLink()) {
|
|
69686
|
+
throw new Error(
|
|
69687
|
+
`Refusing to log in: ${sourceName} points through a symbolic link (${absolute}); use a real operator Claude source directory instead.`
|
|
69688
|
+
);
|
|
69689
|
+
}
|
|
69690
|
+
const canonicalCurrent = await realpath2(current).catch(() => path11.resolve(current));
|
|
69691
|
+
return path11.join(canonicalCurrent, ...missing);
|
|
69692
|
+
}
|
|
69693
|
+
const parent = path11.dirname(current);
|
|
69694
|
+
if (parent === current) {
|
|
69695
|
+
return path11.join(path11.parse(absolute).root, ...missing);
|
|
69696
|
+
}
|
|
69697
|
+
missing.unshift(path11.basename(current));
|
|
69698
|
+
current = parent;
|
|
69699
|
+
}
|
|
69700
|
+
}
|
|
69701
|
+
async function runRunnerLogin(options) {
|
|
69702
|
+
const { io, args, appUrl: appUrl2, env } = options;
|
|
69703
|
+
const homeDir = options.homeDir ?? os3.homedir();
|
|
69704
|
+
const spawnLoginImpl = options.spawnLoginImpl ?? spawnLogin;
|
|
69705
|
+
const spawnStatusImpl = options.spawnStatusImpl ?? spawnStatus;
|
|
69706
|
+
const commitCellMarkerImpl = options.commitCellMarkerImpl ?? ((cell2) => commitCodexHomeSeedMarker(cell2, { required: true }));
|
|
69707
|
+
const materializeFromKeychainImpl = options.materializeFromKeychainImpl ?? ((cell2) => materializeClaudeCredentialFileFromMacosKeychain(cell2));
|
|
69708
|
+
try {
|
|
69709
|
+
let stateFile;
|
|
69710
|
+
let runnerName;
|
|
69711
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
69712
|
+
const token = args[index];
|
|
69713
|
+
if (token === "--state-file") {
|
|
69714
|
+
const value = args[index + 1];
|
|
69715
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
69716
|
+
io.stderr.write("--state-file requires a value.\n");
|
|
69717
|
+
return 1;
|
|
69718
|
+
}
|
|
69719
|
+
stateFile = value;
|
|
69720
|
+
index += 1;
|
|
69721
|
+
} else if (token === "--name") {
|
|
69722
|
+
const value = args[index + 1];
|
|
69723
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
69724
|
+
io.stderr.write("--name requires a value.\n");
|
|
69725
|
+
return 1;
|
|
69726
|
+
}
|
|
69727
|
+
runnerName = value;
|
|
69728
|
+
index += 1;
|
|
69729
|
+
} else if (token !== void 0) {
|
|
69730
|
+
io.stderr.write(`Unexpected runner login argument: ${token}
|
|
69731
|
+
`);
|
|
69732
|
+
io.stderr.write(`Usage: ${cliBrand.binName} runner login [--name <runner-name>] [--state-file <path>]
|
|
69733
|
+
`);
|
|
69734
|
+
return 1;
|
|
69735
|
+
}
|
|
69736
|
+
}
|
|
69737
|
+
if (stateFile !== void 0 && runnerName !== void 0) {
|
|
69738
|
+
io.stderr.write("Pass only one of --name or --state-file.\n");
|
|
69739
|
+
return 1;
|
|
69740
|
+
}
|
|
69741
|
+
let resolvedStateFile;
|
|
69742
|
+
if (stateFile !== void 0) {
|
|
69743
|
+
resolvedStateFile = stateFile;
|
|
69744
|
+
} else if (runnerName !== void 0) {
|
|
69745
|
+
resolvedStateFile = runnerServicePaths(homeDir, runnerName).stateFile;
|
|
69746
|
+
} else if (env.RUNNER_STATE_FILE !== void 0) {
|
|
69747
|
+
resolvedStateFile = env.RUNNER_STATE_FILE;
|
|
69748
|
+
} else {
|
|
69749
|
+
const tmpDefault = defaultRunnerStateFile(appUrl2);
|
|
69750
|
+
if (!await isTrustworthyAutoDetectStateFile(tmpDefault)) {
|
|
69751
|
+
io.stderr.write(
|
|
69752
|
+
`Refusing to auto-detect the paired runner state: ${tmpDefault} exists but is not a private, self-owned regular file (possible pre-planted file in a shared tmp directory). Pass --name <runner-name> or --state-file <path> instead.
|
|
69753
|
+
`
|
|
69754
|
+
);
|
|
69755
|
+
return 1;
|
|
69756
|
+
}
|
|
69757
|
+
const tmpDefaultState = await loadState(tmpDefault);
|
|
69758
|
+
const serviceDir = path11.join(homeDir, "Library", "Application Support", cliBrand.name);
|
|
69759
|
+
const serviceNames = await readdir5(serviceDir).catch(() => []);
|
|
69760
|
+
const candidatePaths = serviceNames.filter((name) => /^runner-.+\.json$/.test(name)).map((name) => path11.join(serviceDir, name));
|
|
69761
|
+
const validServiceStates = (await Promise.all(
|
|
69762
|
+
candidatePaths.map(async (candidatePath) => ({
|
|
69763
|
+
path: candidatePath,
|
|
69764
|
+
state: await loadState(candidatePath)
|
|
69765
|
+
}))
|
|
69766
|
+
)).filter((entry) => entry.state !== null);
|
|
69767
|
+
const totalValid = validServiceStates.length + (tmpDefaultState !== null ? 1 : 0);
|
|
69768
|
+
if (totalValid > 1) {
|
|
69769
|
+
io.stderr.write(
|
|
69770
|
+
`Several paired runner state files found (serve tmpdir default and ${serviceDir}); pass --name <runner-name> so the credential lands in the right runner's cell.
|
|
69771
|
+
`
|
|
69772
|
+
);
|
|
69773
|
+
return 1;
|
|
69774
|
+
} else if (tmpDefaultState !== null) {
|
|
69775
|
+
resolvedStateFile = tmpDefault;
|
|
69776
|
+
} else if (validServiceStates.length === 1) {
|
|
69777
|
+
resolvedStateFile = validServiceStates[0]?.path ?? tmpDefault;
|
|
69778
|
+
} else {
|
|
69779
|
+
resolvedStateFile = tmpDefault;
|
|
69780
|
+
}
|
|
69781
|
+
}
|
|
69782
|
+
const state = await loadState(resolvedStateFile);
|
|
69783
|
+
if (state === null || typeof state.runner_id !== "string") {
|
|
69784
|
+
io.stderr.write(
|
|
69785
|
+
`No paired runner state at ${resolvedStateFile}. Pair this runner first (Settings > Runners, or \`${cliBrand.binName} runner serve --user-code <code>\`).
|
|
69786
|
+
`
|
|
69787
|
+
);
|
|
69788
|
+
return 1;
|
|
69789
|
+
}
|
|
69790
|
+
const cell2 = stableClaudeHomeDir(state, homeDir);
|
|
69791
|
+
const operatorClaudeConfigDir = resolveSourceClaudeConfigDir(env);
|
|
69792
|
+
const operatorClaudeSecureStorageDir = resolveSourceClaudeSecureStorageDir(env);
|
|
69793
|
+
const [canonicalCell, canonicalSecureStorageDir, canonicalConfigDir] = await Promise.all([
|
|
69794
|
+
canonicalPathIfExisting(cell2),
|
|
69795
|
+
canonicalPathIfExisting(operatorClaudeSecureStorageDir, "CLAUDE_SECURESTORAGE_CONFIG_DIR"),
|
|
69796
|
+
canonicalPathIfExisting(operatorClaudeConfigDir, "CLAUDE_CONFIG_DIR")
|
|
69797
|
+
]);
|
|
69798
|
+
if (canonicalSecureStorageDir === canonicalCell) {
|
|
69799
|
+
io.stderr.write(
|
|
69800
|
+
`Refusing to log in: CLAUDE_SECURESTORAGE_CONFIG_DIR points at the runner credential cell (${cell2}), which would make the operator's own Claude read the runner credential.
|
|
69801
|
+
`
|
|
69802
|
+
);
|
|
69803
|
+
return 1;
|
|
69804
|
+
}
|
|
69805
|
+
if (canonicalConfigDir === canonicalCell) {
|
|
69806
|
+
io.stderr.write(
|
|
69807
|
+
`Refusing to log in: CLAUDE_CONFIG_DIR points at the runner credential cell (${cell2}), which would make the operator's own Claude read the runner credential.
|
|
69808
|
+
`
|
|
69809
|
+
);
|
|
69810
|
+
return 1;
|
|
69811
|
+
}
|
|
69812
|
+
const onRepair = (message) => {
|
|
69813
|
+
io.stderr.write(`${redactForLog(message)}
|
|
69814
|
+
`);
|
|
69815
|
+
};
|
|
69816
|
+
const credentialCellLockWaitMs = options.credentialCellLockWaitMs ?? LOGIN_CREDENTIAL_CELL_LOCK_WAIT_MS;
|
|
69817
|
+
const credentialCellLock = await acquireCodexHomeLock(cell2, {
|
|
69818
|
+
waitMs: credentialCellLockWaitMs,
|
|
69819
|
+
// DER-3022 (panel round 3, finding 7): without this, a base-symlink failure on this claude
|
|
69820
|
+
// cell reports itself as a "codex home base" failure — matches the label
|
|
69821
|
+
// `ensureVerifiedCodexHomeDir(cell, onRepair, "claude credential cell")` already uses below.
|
|
69822
|
+
label: "claude credential cell"
|
|
69823
|
+
});
|
|
69824
|
+
if (credentialCellLock === null) {
|
|
69825
|
+
io.stderr.write(
|
|
69826
|
+
`another process is using this runner's credential cell (a running claude turn, or a concurrent \`${cliBrand.binName} runner login\`); wait for it to finish and retry.
|
|
69827
|
+
`
|
|
69828
|
+
);
|
|
69829
|
+
return 1;
|
|
69830
|
+
}
|
|
69831
|
+
try {
|
|
69832
|
+
await ensureVerifiedCodexHomeDir(cell2, onRepair, "claude credential cell");
|
|
69833
|
+
const markerPath = path11.join(path11.dirname(cell2), `${path11.basename(cell2)}.seeded`);
|
|
69834
|
+
try {
|
|
69835
|
+
await rm7(markerPath, { force: true });
|
|
69836
|
+
} catch (error51) {
|
|
69837
|
+
throw new Error(
|
|
69838
|
+
`refusing to run claude runner login: could not clear the existing auth marker ${markerPath}; the server must keep Claude work blocked until login completes (${redactForLog(error51 instanceof Error ? error51.message : String(error51))})`
|
|
69839
|
+
);
|
|
69840
|
+
}
|
|
69841
|
+
await sweepClaudeCredentialCell(cell2, onRepair);
|
|
69842
|
+
const loginEnv = buildLoginEnv(env, cell2);
|
|
69843
|
+
let loginCode;
|
|
69844
|
+
try {
|
|
69845
|
+
loginCode = await spawnLoginImpl("claude", ["auth", "login"], { env: loginEnv });
|
|
69846
|
+
} catch (error51) {
|
|
69847
|
+
if (isEnoent(error51)) {
|
|
69848
|
+
io.stderr.write("claude CLI not found on PATH; install Claude Code first.\n");
|
|
69849
|
+
return 1;
|
|
69850
|
+
}
|
|
69851
|
+
throw error51;
|
|
69852
|
+
}
|
|
69853
|
+
if (loginCode !== 0) {
|
|
69854
|
+
io.stderr.write(
|
|
69855
|
+
`claude auth login exited ${loginCode}; the runner credential was not provisioned.
|
|
69856
|
+
`
|
|
69857
|
+
);
|
|
69858
|
+
return 1;
|
|
69859
|
+
}
|
|
69860
|
+
if (!await hasUsableClaudeCredentialFile(cell2)) {
|
|
69861
|
+
const materialized = await materializeFromKeychainImpl(cell2);
|
|
69862
|
+
if (materialized.status === "written") {
|
|
69863
|
+
io.stdout.write(
|
|
69864
|
+
"notice: Claude Code stored the runner credential in the macOS Keychain; copied it into the runner cell's file store so sandboxed turns can read it.\n"
|
|
69865
|
+
);
|
|
69866
|
+
}
|
|
69867
|
+
}
|
|
69868
|
+
const cellStatusEnv = buildLoginEnv(env, cell2);
|
|
69869
|
+
let cellStatusResult;
|
|
69870
|
+
try {
|
|
69871
|
+
cellStatusResult = await spawnStatusImpl("claude", ["auth", "status", "--json"], { env: cellStatusEnv });
|
|
69872
|
+
} catch {
|
|
69873
|
+
cellStatusResult = { code: 1, stdout: "" };
|
|
69874
|
+
}
|
|
69875
|
+
const cellAccount = parseClaudeAccountIdentity(cellStatusResult);
|
|
69876
|
+
const operatorExplicitClaudeConfigDir = requireNonBlank("CLAUDE_CONFIG_DIR", env.CLAUDE_CONFIG_DIR);
|
|
69877
|
+
const operatorStatusEnv = buildOperatorIdentityEnv(env, operatorExplicitClaudeConfigDir, operatorClaudeSecureStorageDir);
|
|
69878
|
+
let operatorStatusResult;
|
|
69879
|
+
try {
|
|
69880
|
+
operatorStatusResult = await spawnStatusImpl("claude", ["auth", "status", "--json"], { env: operatorStatusEnv });
|
|
69881
|
+
} catch {
|
|
69882
|
+
operatorStatusResult = { code: 1, stdout: "" };
|
|
69883
|
+
}
|
|
69884
|
+
const operatorAccount = parseClaudeAccountIdentity(operatorStatusResult);
|
|
69885
|
+
const identityOutcome = evaluateIdentityCollision(cellAccount, operatorAccount);
|
|
69886
|
+
switch (identityOutcome.kind) {
|
|
69887
|
+
case "collision": {
|
|
69888
|
+
io.stdout.write(
|
|
69889
|
+
"advisory: the runner cell identity matched your own Claude Code login. A local comparison cannot prove a runner-only account; complete the morning second-account proof before production use.\n"
|
|
69890
|
+
);
|
|
69891
|
+
break;
|
|
69892
|
+
}
|
|
69893
|
+
case "no_operator_login": {
|
|
69894
|
+
io.stdout.write(
|
|
69895
|
+
"advisory: no login was found in your own Claude Code config, so the local account comparison could not run. This does not prove a runner-only account; complete the morning second-account proof before production use.\n"
|
|
69896
|
+
);
|
|
69897
|
+
break;
|
|
69898
|
+
}
|
|
69899
|
+
case "cell_identity_unreadable": {
|
|
69900
|
+
io.stdout.write(
|
|
69901
|
+
"advisory: the runner cell's identity could not be read back, so the local account comparison could not run. This does not prove a runner-only account; complete the morning second-account proof before production use.\n"
|
|
69902
|
+
);
|
|
69903
|
+
break;
|
|
69904
|
+
}
|
|
69905
|
+
case "distinct": {
|
|
69906
|
+
io.stdout.write(
|
|
69907
|
+
"advisory: the local runner and operator identities differ, but this comparison cannot prove a runner-only account. Complete the morning second-account proof before production use.\n"
|
|
69908
|
+
);
|
|
69909
|
+
break;
|
|
69910
|
+
}
|
|
69911
|
+
default: {
|
|
69912
|
+
const exhaustive = identityOutcome;
|
|
69913
|
+
throw new Error(`unreachable identity collision outcome: ${JSON.stringify(exhaustive)}`);
|
|
69914
|
+
}
|
|
69915
|
+
}
|
|
69916
|
+
const loginVerifyIsolateDir = await mkdtemp2(path11.join(os3.tmpdir(), "rost-runner-login-verify-"));
|
|
69917
|
+
let sandboxProfilePath = null;
|
|
69918
|
+
try {
|
|
69919
|
+
await provisionIsolatedClaudeConfigDir(loginVerifyIsolateDir, cell2, onRepair);
|
|
69920
|
+
const sandbox = resolveSandboxSpec({
|
|
69921
|
+
mode: parseSandboxMode(env.RUNNER_SANDBOX),
|
|
69922
|
+
platform: process.platform,
|
|
69923
|
+
workspaceDir: os3.tmpdir(),
|
|
69924
|
+
tmpDir: os3.tmpdir(),
|
|
69925
|
+
homeDir,
|
|
69926
|
+
stateFile: resolvedStateFile,
|
|
69927
|
+
runnerBaseDir: path11.join(homeDir, ".rost"),
|
|
69928
|
+
allowNetwork: parseSandboxNetwork(env.RUNNER_SANDBOX_NETWORK).allowNetwork,
|
|
69929
|
+
modelRuntime: "claude",
|
|
69930
|
+
claudeSourceHome: operatorClaudeConfigDir,
|
|
69931
|
+
codexSourceHome: resolveSourceCodexHomeDir(env),
|
|
69932
|
+
claudeSecureStorageSourceHome: operatorClaudeSecureStorageDir,
|
|
69933
|
+
claudeIsolateHomeDir: cell2
|
|
69934
|
+
});
|
|
69935
|
+
if (sandbox.kind === "seatbelt") {
|
|
69936
|
+
sandboxProfilePath = path11.join(os3.tmpdir(), `rost-runner-login-verify-sandbox-${randomUUID2()}.sb`);
|
|
69937
|
+
await writeFile7(sandboxProfilePath, sandbox.profile, {
|
|
69938
|
+
mode: 384,
|
|
69939
|
+
flag: fsConstants5.O_CREAT | fsConstants5.O_EXCL | fsConstants5.O_WRONLY | fsConstants5.O_NOFOLLOW
|
|
69940
|
+
});
|
|
69941
|
+
}
|
|
69942
|
+
const { command, args: args2 } = wrapCommandWithSandbox(sandbox, "claude", ["auth", "status", "--json"], sandboxProfilePath);
|
|
69943
|
+
const probeEnv = buildModelSpawnEnv(env, loginVerifyIsolateDir, void 0, cell2);
|
|
69944
|
+
let statusResult;
|
|
69945
|
+
try {
|
|
69946
|
+
statusResult = await spawnStatusImpl(command, args2, { env: probeEnv });
|
|
69947
|
+
} catch {
|
|
69948
|
+
statusResult = { code: 1, stdout: "" };
|
|
69949
|
+
}
|
|
69950
|
+
let loggedIn = false;
|
|
69951
|
+
if (statusResult.code === 0) {
|
|
69952
|
+
try {
|
|
69953
|
+
const parsed = JSON.parse(statusResult.stdout);
|
|
69954
|
+
loggedIn = parsed.loggedIn === true;
|
|
69955
|
+
} catch {
|
|
69956
|
+
loggedIn = false;
|
|
69957
|
+
}
|
|
69958
|
+
}
|
|
69959
|
+
if (!loggedIn) {
|
|
69960
|
+
io.stderr.write(
|
|
69961
|
+
`login completed but the runner cell cannot read back the credential the way a claude turn would; claude-lane turns would still fail. Retry \`${cliBrand.binName} runner login\`.
|
|
69962
|
+
`
|
|
69963
|
+
);
|
|
69964
|
+
return 1;
|
|
69965
|
+
}
|
|
69966
|
+
const credentialFilePresent = await hasUsableClaudeCredentialFile(cell2);
|
|
69967
|
+
if (!credentialFilePresent) {
|
|
69968
|
+
io.stderr.write(
|
|
69969
|
+
`login completed and reads back as authenticated, but no usable credential file was written to the runner cell's file store (the file must be a self-owned, single-link, non-empty credential JSON) \u2014 it is unreachable from a claude turn, whose sandbox denies the Keychain. Retry \`${cliBrand.binName} runner login\`.
|
|
69970
|
+
`
|
|
69971
|
+
);
|
|
69972
|
+
return 1;
|
|
69973
|
+
}
|
|
69974
|
+
} finally {
|
|
69975
|
+
if (sandboxProfilePath) {
|
|
69976
|
+
await rm7(sandboxProfilePath, { force: true });
|
|
69977
|
+
}
|
|
69978
|
+
await rm7(loginVerifyIsolateDir, { recursive: true, force: true });
|
|
69979
|
+
}
|
|
69980
|
+
try {
|
|
69981
|
+
await commitCellMarkerImpl(cell2);
|
|
69982
|
+
} catch (error51) {
|
|
69983
|
+
io.stderr.write(
|
|
69984
|
+
`Claude credential login completed, but the runner auth marker could not be committed; the server will keep Claude work blocked. Retry \`${cliBrand.binName} runner login\` after fixing the runner cell: ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
|
|
69985
|
+
`
|
|
69986
|
+
);
|
|
69987
|
+
return 1;
|
|
69988
|
+
}
|
|
69989
|
+
io.stdout.write(
|
|
69990
|
+
`Runner credential provisioned for runner ${state.runner_id}.
|
|
69991
|
+
Claude-lane turns will authenticate from the dedicated cell at ${cell2}.
|
|
69992
|
+
Your own Claude login was read once for an advisory comparison; complete the morning second-account proof before production use. Claude-lane turns use the dedicated cell, not the operator source store.
|
|
69993
|
+
`
|
|
69994
|
+
);
|
|
69995
|
+
return 0;
|
|
69996
|
+
} finally {
|
|
69997
|
+
await credentialCellLock.release();
|
|
69998
|
+
}
|
|
69999
|
+
} catch (error51) {
|
|
70000
|
+
io.stderr.write(`${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
|
|
70001
|
+
`);
|
|
70002
|
+
return 1;
|
|
70003
|
+
}
|
|
70004
|
+
}
|
|
70005
|
+
function isEnoent(error51) {
|
|
70006
|
+
return typeof error51 === "object" && error51 !== null && "code" in error51 && error51.code === "ENOENT";
|
|
70007
|
+
}
|
|
70008
|
+
|
|
69087
70009
|
// src/index.ts
|
|
69088
70010
|
function warnImplementationCredentialFallback(io, hasUserSession) {
|
|
69089
70011
|
if (!hasUserSession) {
|
|
@@ -69153,6 +70075,15 @@ function createConfirmPrompt(input, output) {
|
|
|
69153
70075
|
};
|
|
69154
70076
|
}
|
|
69155
70077
|
var defaultConfirmPrompt = createConfirmPrompt(process.stdin, process.stderr);
|
|
70078
|
+
function createUnavailableCredentialStore(error51) {
|
|
70079
|
+
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
70080
|
+
return {
|
|
70081
|
+
read: () => Promise.resolve(null),
|
|
70082
|
+
write: () => Promise.reject(new Error(`No OS credential store is available; cannot store a session. ${message}`)),
|
|
70083
|
+
clear: () => Promise.reject(new Error(`No OS credential store is available; cannot clear a session. ${message}`)),
|
|
70084
|
+
describe: () => "no OS credential store is available"
|
|
70085
|
+
};
|
|
70086
|
+
}
|
|
69156
70087
|
async function main(argv = process.argv.slice(2), options = {}) {
|
|
69157
70088
|
const io = options.io ?? {
|
|
69158
70089
|
stdout: process.stdout,
|
|
@@ -69298,6 +70229,24 @@ async function main(argv = process.argv.slice(2), options = {}) {
|
|
|
69298
70229
|
`);
|
|
69299
70230
|
return 1;
|
|
69300
70231
|
}
|
|
70232
|
+
if (command === "runner" && args[0] === "login") {
|
|
70233
|
+
const config3 = loadConfig();
|
|
70234
|
+
return runRunnerLogin({
|
|
70235
|
+
io,
|
|
70236
|
+
args: args.slice(1),
|
|
70237
|
+
appUrl: config3.appUrl,
|
|
70238
|
+
env: process.env
|
|
70239
|
+
});
|
|
70240
|
+
}
|
|
70241
|
+
if (command === "runner" && isRunnerServiceInvocation(args)) {
|
|
70242
|
+
const serviceConfigResult = loadConfigWithDiagnostics();
|
|
70243
|
+
return runRunnerService({
|
|
70244
|
+
io,
|
|
70245
|
+
args,
|
|
70246
|
+
appUrl: serviceConfigResult.config.appUrl,
|
|
70247
|
+
allowDevelopmentAppUrl: process.env[allowInsecureAppUrlEnv] === "1"
|
|
70248
|
+
});
|
|
70249
|
+
}
|
|
69301
70250
|
let store;
|
|
69302
70251
|
try {
|
|
69303
70252
|
store = options.store ?? createTokenStore();
|
|
@@ -69305,7 +70254,11 @@ async function main(argv = process.argv.slice(2), options = {}) {
|
|
|
69305
70254
|
if (command === "command" && (args[0] === "schema" || args[0] === "list")) {
|
|
69306
70255
|
return runLocalCommandDiscovery(io, args, cliBrand.binName);
|
|
69307
70256
|
}
|
|
69308
|
-
|
|
70257
|
+
if (command === "runner" && args[0] === "serve") {
|
|
70258
|
+
store = createUnavailableCredentialStore(error51);
|
|
70259
|
+
} else {
|
|
70260
|
+
throw error51;
|
|
70261
|
+
}
|
|
69309
70262
|
}
|
|
69310
70263
|
if (command === "login") {
|
|
69311
70264
|
const config3 = loadConfig();
|
|
@@ -69382,14 +70335,6 @@ async function main(argv = process.argv.slice(2), options = {}) {
|
|
|
69382
70335
|
env: process.env
|
|
69383
70336
|
});
|
|
69384
70337
|
}
|
|
69385
|
-
if (command === "runner" && isRunnerServiceInvocation(args)) {
|
|
69386
|
-
return runRunnerService({
|
|
69387
|
-
io,
|
|
69388
|
-
args,
|
|
69389
|
-
appUrl: config2.appUrl,
|
|
69390
|
-
allowDevelopmentAppUrl: process.env[allowInsecureAppUrlEnv] === "1"
|
|
69391
|
-
});
|
|
69392
|
-
}
|
|
69393
70338
|
const implCredentialReader = createImplementationCredentialReader();
|
|
69394
70339
|
if (command === "onboard" && (args[0] === "status" || args[0] === "resume" || args[0] === "rehearse" || args[0] === "activate" || args[0] === "source-ingest" || args[0] === "setup" || args[0] === "setup-status")) {
|
|
69395
70340
|
const implCredential = await implCredentialReader.read();
|