Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/aw/work-queue.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ producer entitlement. Never give agent execution or snapshot MCP queue-write
credentials. Read the [deployment guide](../../docs/src/content/docs/guides/deploy-work-queue.md)
only for installation tasks; writer-restriction automation remains deferred.

## Mirror admitted Work with Issues

`tools.work-queue.issues: true` mirrors admitted Work with the `work` label.
An object may set `label` and a pre-provisioned native organization
`status-field`. Require installed projector authority: only protected hooks
project their own admissions/original Claims. Immutable `backing_issue` binds
one Work per Issue. Agents never write mirrors; human edits never establish
Result. Uncertain writes stay pending, never recreated or globally repaired.
Upgrade all closed-schema readers first. See the
[backing Issue reference](../../docs/src/content/docs/reference/work-queue.md#backing-issues).

## Plan and dispatch

- Use `work_queue_read` / `work_queue_explain` for the immutable activation
Expand Down
121 changes: 121 additions & 0 deletions actions/setup/js/work_queue_checked_transport.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// @ts-check
"use strict";

const { canonical, parseStrictJSON, queueError } = require("./work_queue_codec.cjs");
const { replayTransactionLog, replayTransactions } = require("./work_queue_replay.cjs");
const { validateBranch } = require("./work_queue_store.cjs");

const MAX_BYTES = 80 * 1024 * 1024;
const JOURNAL_PATH = /^\.gh-aw\/issue-projection\/[a-f0-9]{64}\.json$/;

async function checkedBlobText(githubClient, owner, repo, blob, oid, maxBytes) {
if (!blob || blob.oid !== oid || !Number.isSafeInteger(blob.byteSize) || blob.byteSize > maxBytes || blob.byteSize < 0) throw queueError("ledger_invalid", "blob does not belong to the immutable checked head or is oversized");
if (blob.isTruncated === false && typeof blob.text === "string" && blob.byteSize === Buffer.byteLength(blob.text, "utf8")) return blob.text;
if (blob.isTruncated !== true) throw queueError("ledger_invalid", "checked blob is unreadable");
const response = await githubClient.rest.git.getBlob({ owner, repo, file_sha: oid, request: { retries: 0 } });
const value = response?.data;
if (value?.sha !== oid || value.encoding !== "base64" || typeof value.content !== "string" || value.size !== blob.byteSize) throw queueError("ledger_invalid", "immutable blob fallback identity or encoding is invalid");
const encoded = value.content.replace(/\s/g, "");
if (encoded.length > Math.ceil(maxBytes / 3) * 4 || encoded.length % 4 !== 0 || /[^A-Za-z0-9+/=]/.test(encoded)) throw queueError("ledger_invalid", "immutable blob fallback is malformed or oversized");
const bytes = Buffer.from(encoded, "base64");
if (bytes.length !== blob.byteSize || bytes.toString("base64") !== encoded) throw queueError("ledger_invalid", "immutable blob fallback is truncated or malformed");
try {
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
} catch {
throw queueError("ledger_invalid", "immutable blob fallback contains malformed UTF-8");
}
}

/** @param {import("./work_queue_store.cjs").QueueReadOptions & {paths?: string[], issueRead?: ReturnType<typeof import("./work_queue_issue_api.cjs").issueReadQuery>}} options */
async function readCheckedQueue({ githubClient, owner, repo, branch = "work-queue", paths = [], issueRead = undefined }) {
validateBranch(branch);
if (paths.length > 25 || paths.some(path => !JOURNAL_PATH.test(path))) throw queueError("projection_invalid", "invalid bounded projection journal paths");
const journals = paths.map((path, index) => `j${index}: object(expression:$j${index}) { oid ... on Blob { text byteSize isTruncated } }`).join("\n");
const variables = { owner, repo, ref: `refs/heads/${branch}`, log: `refs/heads/${branch}:work-queue.jsonl` };
paths.forEach((path, index) => {
variables[`j${index}`] = `refs/heads/${branch}:${path}`;
});
const parameters = paths.map((_, index) => `$j${index}:String!`).join(",");
const nativeParameters = issueRead?.declarations?.join(",") || "";
Object.assign(variables, issueRead?.variables || {});
const response = await githubClient.graphql(
`query CheckedWorkQueue($owner:String!,$repo:String!,$ref:String!,$log:String!${parameters ? "," + parameters : ""}${nativeParameters ? "," + nativeParameters : ""}) {
repository(owner:$owner,name:$repo) {
id nameWithOwner isEmpty defaultBranchRef { target { oid } }
legacy0: ref(qualifiedName:"refs/heads/dispatch-coordinator") { target { oid } }
legacy1: ref(qualifiedName:"refs/heads/gh-aw-work-queue") { target { oid } }
ref(qualifiedName:$ref) { target { ... on Commit { oid tree { oid entries { name type mode oid object {
... on Tree { entries { name type mode oid object { ... on Tree { entries { name type mode oid } } } } }
} } } } } }
log: object(expression:$log) { oid ... on Blob { text byteSize isTruncated } }
${journals}
}
${issueRead?.selections?.join("\n") || ""}
}`,
variables
);
const repository = response?.repository;
if (!repository || repository.nameWithOwner.toLowerCase() !== `${owner}/${repo}`.toLowerCase()) throw queueError("repository_unavailable", "checked queue repository identity is unavailable");
if (repository.ref === null) {
if (repository.isEmpty !== true && !repository.defaultBranchRef?.target?.oid) throw queueError("repository_unavailable", "cannot establish contents access before treating a queue as absent");
if (branch === "work-queue" && (repository.legacy0 || repository.legacy1)) throw queueError("unsupported_protocol", "a legacy queue cannot be implicitly adopted");
return { sha: null, treeSha: null, transactions: [], state: replayTransactions([]), branch, logPath: "work-queue.jsonl", repositoryId: repository.id, journal: new Map() };
}
const commit = repository.ref?.target;
if (!commit?.oid || !commit.tree?.oid || !Array.isArray(commit.tree.entries)) throw queueError("ledger_invalid", "checked transport requires an initialized queue");
const entries = commit.tree.entries;
if (entries.length > 16384) throw queueError("resource_limit", "queue tree entry limit exceeded");
if (entries.some(entry => entry.name === "dispatch-work-coordinator.jsonl")) throw queueError("unsupported_protocol", "legacy queue storage is unsupported");
const logs = entries.filter(entry => entry.name === "work-queue.jsonl");
const entry = logs[0];
if (logs.length !== 1 || entry.type !== "blob" || entry.mode !== 33188) throw queueError("ledger_invalid", "queue requires one regular canonical log");
const text = await checkedBlobText(githubClient, owner, repo, repository.log, entry.oid, MAX_BYTES);
const state = replayTransactionLog(text);
if (state.transactions.some(transaction => transaction.actor.repository.toLowerCase() !== repository.nameWithOwner.toLowerCase())) throw queueError("actor_unauthorized", "ledger contains a foreign repository");
const journal = new Map();
const directory = entries.find(entry => entry.name === ".gh-aw")?.object?.entries?.find(entry => entry.name === "issue-projection")?.object?.entries || [];
for (const [index, path] of paths.entries()) {
const value = repository[`j${index}`];
const expected = directory.find(entry => entry.name === path.split("/").at(-1));
if (!expected && value === null) continue;
if (!expected || expected.type !== "blob" || expected.mode !== 33188 || !value || value.oid !== expected.oid) throw queueError("projection_journal_conflict", "journal does not belong to the immutable checked ledger head");
journal.set(path, parseStrictJSON(await checkedBlobText(githubClient, owner, repo, value, expected.oid, 1024 * 1024), { maxBytes: 1024 * 1024 }));
}
return { sha: commit.oid, treeSha: commit.tree.oid, transactions: state.transactions, state, branch, logPath: "work-queue.jsonl", repositoryId: repository.id, journal, nativeResponse: issueRead ? response : undefined };
}

async function writeCheckedFiles({ githubClient, owner, repo, branch, expectedHeadOid, files }) {
validateBranch(branch);
if (
!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(expectedHeadOid) ||
files.length < 1 ||
files.length > 26 ||
new Set(files.map(file => file.path)).size !== files.length ||
files.some(
file =>
typeof file.path !== "string" ||
(file.path !== "work-queue.jsonl" && !JOURNAL_PATH.test(file.path)) ||
typeof file.content !== "string" ||
Buffer.byteLength(file.content, "utf8") > (file.path === "work-queue.jsonl" ? MAX_BYTES : 1024 * 1024)
)
)
throw queueError("publication_invalid", "checked publication requires explicit head and files");
const response = await githubClient.graphql("mutation CheckedWorkQueuePublication($input:CreateCommitOnBranchInput!) { createCommitOnBranch(input:$input) { commit { oid } } }", {
input: {
branch: { repositoryNameWithOwner: `${owner}/${repo}`, branchName: branch },
expectedHeadOid,
message: { headline: "Publish checked work queue projection" },
fileChanges: { additions: files.map(file => ({ path: file.path, contents: Buffer.from(file.content, "utf8").toString("base64") })) },
},
request: { retries: 0, timeout: 30000 },
});
const oid = response?.createCommitOnBranch?.commit?.oid;
if (typeof oid !== "string" || !/^[a-f0-9]{40}$|^[a-f0-9]{64}$/.test(oid)) throw queueError("publication_unresolved", "checked publication returned no commit identity");
return oid;
}

function writeCheckedCandidate({ githubClient, owner, repo, current, transactions }) {
return writeCheckedFiles({ githubClient, owner, repo, branch: current.branch, expectedHeadOid: current.sha, files: [{ path: current.logPath, content: transactions.map(commit => canonical(commit)).join("\n") + "\n" }] });
}

module.exports = { readCheckedQueue, writeCheckedFiles, writeCheckedCandidate };
2 changes: 2 additions & 0 deletions actions/setup/js/work_queue_dispatch.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ async function resolveAdmissionResources(options, state, parameters, trustedCont
references.push({ target: edge, field: "resource", resource: { ...edge.resource, condition: edge.condition } });
}
if (node.subject) references.push({ target: node, field: "subject", resource: { ...node.subject, condition: node.subject.kind === "issue" ? "completed" : "merged" } });
if (node.backing_issue) references.push({ target: node, field: "backing_issue", resource: { ...node.backing_issue, condition: "completed" } });
}
if (!references.length) return parameters;
log.debug("admission.resolve.references", { references: references.length });
Expand Down Expand Up @@ -308,6 +309,7 @@ function acceptedSubmissionParameters(state, trustedContext, parameters, prior)
return edge.kind !== "work" && original?.kind === edge.kind && original.condition === edge.condition ? { ...edge, resource: restoreIdentity(edge.resource, original.resource) } : edge;
});
if (node.subject) node.subject = restoreIdentity(node.subject, accepted?.subject);
if (node.backing_issue) node.backing_issue = restoreIdentity(node.backing_issue, accepted?.backing_issue);
}
return normalized;
}
Expand Down
4 changes: 3 additions & 1 deletion actions/setup/js/work_queue_documentation.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
it("omits removed storage selectors from the generated frontmatter reference", () => {
const source = readRepositoryFile("docs/src/content/docs/reference/frontmatter-full.md");
const queue = source.slice(source.indexOf(" # Read the immutable version-3 activation snapshot."), source.indexOf(" # Cache memory MCP configuration"));
expect(queue).toContain("work-queue: true");

Check failure on line 21 in actions/setup/js/work_queue_documentation.test.cjs

View workflow job for this annotation

GitHub Actions / impacted-js-tests

work_queue_documentation.test.cjs > work-queue deployment documentation > omits removed storage selectors from the generated frontmatter reference

AssertionError: expected ' # Read the immutable version-3 acti…' to contain 'work-queue: true' - Expected + Received - work-queue: true + # Read the immutable version-3 activation snapshot. Nonworkers without approved + # dispatch targets are read-only observers; workers and approved dispatchers may + # stage scoped queue intents. The installed ledger Policy is authoritative. Only + # Git storage is supported. + # (optional) + # Accepted formats: + + # Format 1: Enable read-only queue observation, or queue-control intents when + # approved worker dispatch targets are configured. + + # Format 2: Enable the work-queue MCP server. + work-queue: null + + # Format 3: object + work-queue: + # Fail closed and block safe outputs when no trusted inbound worker assignment is + # present. + # (optional) + require-assignment: true + + # Declare this workflow as a work-queue worker eligible to receive trusted queue + # claims. + # (optional) + worker: true + + # Mirror only this run's admitted Work and original authenticated Claims to GitHub + # Issues. Git remains authoritative; requires installed projector rules. + # (optional) + # Accepted formats: + + # Format 1: boolean + issues: true + + # Format 2: object + issues: + # (optional) + label: "example-value" + + # Pre-provisioned native organization single-select field. Omit for comment-only + # status. + # (optional) + status-field: "example-value" + + # Declare one schema-validated JSON memory snapshot per Claim. The compiler + # supplies credential-free preparation and protected immutable git-tree delivery. + # (optional) + memory: + # (optional) + name: "My Workflow" + + # Fixed relative JSON file path in the snapshot tree. + path: "example-value" + + # Fixed repository also authorized by the installed immutable Work scope. + target-repo: "example-value" + + base-revision: "example-value" + + branch-prefix: "example-value" + + # (optional) + max-bytes: 1 + + # Bounded inline typed JSON Schema subset: object + # properties/required/additionalProperties, array items, scalar enum, numeric + # ranges, and string/array length bounds. No references, regexes, or executable + # validators. + schema: + {} + + ❯ work_queue_documentation.test.cjs:21:19

Check failure on line 21 in actions/setup/js/work_queue_documentation.test.cjs

View workflow job for this annotation

GitHub Actions / JS Tests (shard 2/4)

work_queue_documentation.test.cjs > work-queue deployment documentation > omits removed storage selectors from the generated frontmatter reference

AssertionError: expected ' # Read the immutable version-3 acti…' to contain 'work-queue: true' - Expected + Received - work-queue: true + # Read the immutable version-3 activation snapshot. Nonworkers without approved + # dispatch targets are read-only observers; workers and approved dispatchers may + # stage scoped queue intents. The installed ledger Policy is authoritative. Only + # Git storage is supported. + # (optional) + # Accepted formats: + + # Format 1: Enable read-only queue observation, or queue-control intents when + # approved worker dispatch targets are configured. + + # Format 2: Enable the work-queue MCP server. + work-queue: null + + # Format 3: object + work-queue: + # Fail closed and block safe outputs when no trusted inbound worker assignment is + # present. + # (optional) + require-assignment: true + + # Declare this workflow as a work-queue worker eligible to receive trusted queue + # claims. + # (optional) + worker: true + + # Mirror only this run's admitted Work and original authenticated Claims to GitHub + # Issues. Git remains authoritative; requires installed projector rules. + # (optional) + # Accepted formats: + + # Format 1: boolean + issues: true + + # Format 2: object + issues: + # (optional) + label: "example-value" + + # Pre-provisioned native organization single-select field. Omit for comment-only + # status. + # (optional) + status-field: "example-value" + + # Declare one schema-validated JSON memory snapshot per Claim. The compiler + # supplies credential-free preparation and protected immutable git-tree delivery. + # (optional) + memory: + # (optional) + name: "My Workflow" + + # Fixed relative JSON file path in the snapshot tree. + path: "example-value" + + # Fixed repository also authorized by the installed immutable Work scope. + target-repo: "example-value" + + base-revision: "example-value" + + branch-prefix: "example-value" + + # (optional) + max-bytes: 1 + + # Bounded inline typed JSON Schema subset: object + # properties/required/additionalProperties, array items, scalar enum, numeric + # ranges, and string/array length bounds. No references, regexes, or executable + # validators. + schema: + {} + + ❯ work_queue_documentation.test.cjs:21:19
expect(queue).toContain("worker: true");
expect(queue).not.toMatch(/\bstorage:/);
});
Expand All @@ -26,13 +26,15 @@
it("installs the documented policy after replacing identity placeholders", () => {
const source = readRepositoryFile(deploymentPath);
const examples = [...source.matchAll(/```json(?: [^\n]*)?\n([\s\S]*?)\n```/g)];
expect(examples).toHaveLength(1);
expect(examples).toHaveLength(2);
const policy = JSON.parse(examples[0][1].replaceAll("REPLACE_WITH_PRODUCER_ACTOR_ID", "11").replaceAll("REPLACE_WITH_WORKER_CREDENTIAL_ACTOR_ID", "12").replaceAll("REPLACE_WITH_40_OR_64_HEX_COMMIT_SHA", "a".repeat(40)));

expect(validatePolicy(policy)).toBe(policy);
expect(policy.accounting_weights).toEqual({ "": 1 });
expect(policy.producers["11"].fairness_keys).toEqual([""]);
expect(policy.limits).toEqual(DEFAULT_LIMITS);
const projection = JSON.parse(examples[1][1].replaceAll("REPLACE_WITH_NATIVE_PRINCIPAL_ID", "12").replaceAll("REPLACE_WITH_40_OR_64_HEX_COMMIT_SHA", "a".repeat(40)));
expect(validatePolicy({ ...policy, ...projection }).projectors).toEqual(projection.projectors);
});

it("separates published docs and specifications from bounded agent instructions", () => {
Expand Down
25 changes: 23 additions & 2 deletions actions/setup/js/work_queue_graph.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,13 @@ function newChildWork(state, actor, payload, node_key, at) {
}

function validateWork(node, state, actor) {
closed(node, ["kind", "work_id", "graph_id", "node_key", "pool", "priority", "fairness_key", "worker_profile", "batch_trust_domain", "payload", "depends_on", "enqueued"], ["subject", "replacement_of"], "Work", "unsupported_protocol");
closed(
node,
["kind", "work_id", "graph_id", "node_key", "pool", "priority", "fairness_key", "worker_profile", "batch_trust_domain", "payload", "depends_on", "enqueued"],
["subject", "backing_issue", "replacement_of"],
"Work",
"unsupported_protocol"
);
if (node.kind !== "Work") throw queueError("unsupported_protocol", "graph admission requires Work operations");
for (const field of ["work_id", "graph_id", "node_key", "pool", "worker_profile", "batch_trust_domain"]) identity(node[field], field);
if (node.work_id !== nodeId(node.graph_id, node.node_key)) throw queueError("work_identity_invalid", "Work identity must bind graph_id/node_key");
Expand All @@ -92,6 +98,10 @@ function validateWork(node, state, actor) {
validateResource(node.subject);
if (!pool.allowed_repositories.includes(node.subject.repository)) throw queueError("resource_unauthorized", "subject repository is not allowlisted");
}
if (Object.hasOwn(node, "backing_issue")) {
validateResource(node.backing_issue);
if (node.backing_issue.kind !== "issue" || !pool.allowed_repositories.includes(node.backing_issue.repository)) throw queueError("resource_unauthorized", "backing Issue must be an allowlisted Issue");
}
if (Object.hasOwn(node, "replacement_of")) {
closed(node.replacement_of, ["work_id", "disposition", "evidence"], [], "replacement");
identity(node.replacement_of.evidence, "remediation evidence");
Expand Down Expand Up @@ -130,7 +140,7 @@ function validateWork(node, state, actor) {
}

function workDefinition(work) {
const fields = ["kind", "work_id", "graph_id", "node_key", "pool", "priority", "fairness_key", "worker_profile", "batch_trust_domain", "payload", "depends_on", "enqueued", "subject", "replacement_of"];
const fields = ["kind", "work_id", "graph_id", "node_key", "pool", "priority", "fairness_key", "worker_profile", "batch_trust_domain", "payload", "depends_on", "enqueued", "subject", "backing_issue", "replacement_of"];
return Object.fromEntries(fields.filter(field => Object.hasOwn(work, field)).map(field => [field, work[field]]));
}

Expand All @@ -139,10 +149,21 @@ function validateGraphAdmission(state, nodes, actor) {
const admitted = new Map();
const candidate = id => admitted.get(id) || state.works.get(id);
const seen = new Set();
const issues = new Map();
const issueKey = resource => canonical([resource.host, resource.repository_id, resource.resource_id]);
for (const work of state.works.values()) {
const resource = work.backing_issue || work.issue_link;
if (resource) issues.set(issueKey(resource), work.work_id);
}
for (const node of nodes) {
validateWork(node, state, actor);
if (seen.has(node.work_id)) throw queueError("graph_invalid", "duplicate node key within one submission");
seen.add(node.work_id);
if (node.backing_issue) {
const key = issueKey(node.backing_issue);
if (issues.has(key) && issues.get(key) !== node.work_id) throw queueError("issue_binding_conflict", "one Work per backing Issue");
issues.set(key, node.work_id);
}
const existing = state.works.get(node.work_id);
if (existing && canonical(workDefinition(existing)) !== canonical(node)) throw queueError("work_conflict", "immutable node definition differs");
if (!existing && state.admission_paused) throw queueError("admission_paused", "new Work admission is paused");
Expand Down
3 changes: 2 additions & 1 deletion actions/setup/js/work_queue_intents.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ function normalizeSubmitParameters(parameters, policy, at, state) {
integer(at, 0, Number.MAX_SAFE_INTEGER, "submission timestamp");
return {
nodes: parameters.nodes.map(node => {
closed(node, ["payload"], ["graph_id", "node_key", "work_id", "pool", "priority", "fairness_key", "worker_profile", "depends_on", "subject", "replacement_of"], "submitted Work");
closed(node, ["payload"], ["graph_id", "node_key", "work_id", "pool", "priority", "fairness_key", "worker_profile", "depends_on", "subject", "backing_issue", "replacement_of"], "submitted Work");
const graphId = Object.hasOwn(node, "graph_id") ? identity(node.graph_id, "graph ID") : digest(node.payload);
const nodeKey = Object.hasOwn(node, "node_key") ? identity(node.node_key, "node key") : "root";
const poolName = node.pool === undefined ? "default" : node.pool;
Expand All @@ -138,6 +138,7 @@ function normalizeSubmitParameters(parameters, policy, at, state) {
depends_on: node.depends_on === undefined ? [] : node.depends_on,
enqueued: state?.works.get(workId)?.enqueued ?? at,
...(node.subject === undefined ? {} : { subject: node.subject }),
...(node.backing_issue === undefined ? {} : { backing_issue: node.backing_issue }),
...(node.replacement_of === undefined ? {} : { replacement_of: node.replacement_of }),
};
}),
Expand Down
Loading
Loading