mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 07:55:47 +01:00
Reconcile Claude platform packages with merged Codex defaults
Co-Authored-By: Paperclip <noreply@paperclip.ing> * codex/claude-runner-default: (85 commits) feat: default new Codex agents to Paperclip Runner on Linux and macOS (#15683) docs(skills): add slow-query diagnosis and fix skill (#16303) feat(sentry): add opt-in run failure reporting with saved run references (#16300) refactor(server): isolate provider message admission and runtime policies (#16289) refactor(server): isolate provider webhook lifecycle decisions (#16283) fix(workspaces): keep cleanup Git scans fresh and dedupe page reads (#16302) fix(sentry): keep provider-rejected Grok wakes out of run failure reports (#16297) fix(runner): report Runner disconnect age when a Dot bridge exits (#16296) refactor(server): isolate chat conversation and control policies (#16276) perf(recovery): use indexed execution identity lookups (#16291) perf(runs): bound history and reduce repeated context reads (#16286) fix(native-runtime): report which close barrier failed (#16298) feat(skills): select and validate tracked GitHub branches (#16288) fix(observability): sample native runs independently of triggers (#16285) fix(github): retry and recover review publication (#16284) perf(workspaces): wake terminal cleanup from committed changes (#16282) fix(mcp): deduplicate discovered tool descriptors (#16281) perf: remove history scans from task and chat queries (#16278) docs(skills): add maintenance bot setup workflow (#16279) fix(observability): time execution configuration steps (#16236) ... # Conflicts: # scripts/verify-grok-npm-install.mjs
This commit is contained in:
commit
b030683ca2
1176 files changed
+129461
-261897
No files matched your search
@@ -11,10 +11,41 @@ describe("explicit diagnostic credential forms", () => {
|
||||
});
|
||||
import {
|
||||
REDACTED_COMMAND_TEXT_VALUE,
|
||||
looksLikeCredentialJwt,
|
||||
redactDiagnosticText,
|
||||
redactCommandText,
|
||||
} from "./command-redaction.js";
|
||||
|
||||
describe("looksLikeCredentialJwt", () => {
|
||||
it("recognizes encoded headers even with short payloads and signatures", () => {
|
||||
const header = Buffer.from('{"alg":"HS256"}').toString("base64url");
|
||||
expect(looksLikeCredentialJwt(`${header}.e30.c2ln`)).toBe(true);
|
||||
});
|
||||
|
||||
it("recognizes direct-encryption JWTs with an empty encrypted-key segment", () => {
|
||||
const header = Buffer.from('{"alg":"dir","enc":"A256GCM"}').toString("base64url");
|
||||
expect(looksLikeCredentialJwt(`${header}..abcdefghijk.abcdefghijkl.abcdefghijk`)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"eyJ..signature",
|
||||
"eyJ.key..ciphertext.tag",
|
||||
"eyJ.key.iv..tag",
|
||||
"eyJ.key.iv.ciphertext.",
|
||||
])("rejects empty segments outside the JWE encrypted key: %s", (input) => {
|
||||
expect(looksLikeCredentialJwt(input)).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"api.openai.com",
|
||||
"agents.staging.example.com",
|
||||
"deployment.credentials.example.md",
|
||||
"paperclip.native-model-envelope.v3",
|
||||
])("rejects public dotted identifiers: %s", (input) => {
|
||||
expect(looksLikeCredentialJwt(input)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("redactDiagnosticText", () => {
|
||||
it("preserves credential metadata and dotted identifiers", () => {
|
||||
for (const text of [
|
||||
|
||||
@@ -36,7 +36,12 @@ const COMMAND_JWT_RE =
|
||||
/** Recognize encoded JSON headers, without treating dotted identifiers as tokens. */
|
||||
export function looksLikeCredentialJwt(value: string): boolean {
|
||||
const segments = value.split(".");
|
||||
if (![3, 5].includes(segments.length) || segments.some((part) => !/^[A-Za-z0-9_-]{8,}$/.test(part))) return false;
|
||||
if (![3, 5].includes(segments.length)) return false;
|
||||
if (segments.some((part, index) => {
|
||||
// Compact JWE direct encryption has no encrypted-key value.
|
||||
if (segments.length === 5 && index === 1 && part === "") return false;
|
||||
return !/^[A-Za-z0-9_-]+$/.test(part);
|
||||
})) return false;
|
||||
if (segments[0].startsWith("eyJ")) return true;
|
||||
try {
|
||||
const header = JSON.parse(atob(segments[0].replace(/-/g, "+").replace(/_/g, "/")));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type PaperclipRunnerProvider =
|
||||
"codex" | "opencode" | "claude_managed" | "aws_agentcore" | "acpx" | "openai_dot";
|
||||
"codex" | "opencode" | "claude_managed" | "aws_agentcore" | "acpx" | "openai_dot" | "grok_bot";
|
||||
|
||||
export type CodexPermissionMode = "never" | "on-request" | "untrusted";
|
||||
export type OpenCodePermissionMode = "allow" | "ask" | "deny";
|
||||
@@ -103,6 +103,7 @@ export const PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES = {
|
||||
description:
|
||||
"AWS AgentCore runs non-interactively under its qualified harness profile and Paperclip policy.",
|
||||
},
|
||||
grok_bot: { configurable: false, defaultMode: "provider-managed", options: [], description: "Grok Bot uses first-party Paperclip tools and retains its native apps. Grok manages its model; spending, live steering, provider transcript streaming and remote interruption are unavailable." },
|
||||
openai_dot: { configurable: false, defaultMode: "provider-managed", options: [], description: "Dot uses the admitted Paperclip tool catalog. OpenAI manages its model and other tools; provider usage and global interruption are unavailable." },
|
||||
acpx: {
|
||||
configurable: true,
|
||||
@@ -148,7 +149,7 @@ export function isPaperclipRunnerProvider(
|
||||
value === "opencode" ||
|
||||
value === "claude_managed" ||
|
||||
value === "aws_agentcore" ||
|
||||
value === "acpx" || value === "openai_dot"
|
||||
value === "acpx" || value === "openai_dot" || value === "grok_bot"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1335,9 +1335,6 @@ export async function prepareSandboxManagedRuntime(input: {
|
||||
);
|
||||
const workspaceRestoreExclude = mergeExcludes(restoreExclude, repositories.map((repo) => repo.path));
|
||||
// The baseline "before" snapshot: a recursive walk of the whole workspace that
|
||||
// `lstat`s every entry and SHA-256-hashes every file's bytes. This is the
|
||||
// dominant cost in the pre-`pack` window — it reads the content of every
|
||||
// non-excluded file, serially — so it earns its own span.
|
||||
baselineSnapshot = syncWorkspace
|
||||
? (input.workspaceBaseline ??
|
||||
(await runStepSpan("snapshot.baseline", () =>
|
||||
|
||||
@@ -370,7 +370,6 @@ export interface AdapterEnvironmentTestContext {
|
||||
companyId: string;
|
||||
adapterType: string;
|
||||
config: Record<string, unknown>;
|
||||
/** Server-owned account materialization; native probes copy refreshed auth back before cleanup. */
|
||||
managedAiCredentialHome?: string;
|
||||
/**
|
||||
* Optional execution target the adapter should run probes against.
|
||||
@@ -728,7 +727,6 @@ export interface CreateConfigValues {
|
||||
resolvedRunner?: "paperclip" | "legacy";
|
||||
adapterType: string;
|
||||
codexPermissionMode?: "never" | "on-request" | "untrusted";
|
||||
/** Explicit Legacy permission choice; never persisted as a runner setting. */
|
||||
codexLegacySandboxBypass?: boolean;
|
||||
paperclipRunnerLifecycleMode?: "per_turn" | "warm";
|
||||
paperclipRunnerIdleTimeoutMs?: number;
|
||||
|
||||
@@ -147,21 +147,37 @@ async function hashFile(filePath: string): Promise<string> {
|
||||
async function* walkDirectory(
|
||||
root: string, exclude: readonly string[], ignored: ReturnType<typeof workspacePathMatcher>, relative = "",
|
||||
): AsyncGenerator<[string, SnapshotEntry]> {
|
||||
const readEntry = async (nextRelative: string): Promise<[string, SnapshotEntry | null]> => {
|
||||
const fullPath = path.join(root, nextRelative);
|
||||
const stats = await fs.lstat(fullPath);
|
||||
if (stats.isDirectory()) return [nextRelative, { kind: "dir" }];
|
||||
if (stats.isSymbolicLink()) return [nextRelative, { kind: "symlink", target: await fs.readlink(fullPath) }];
|
||||
if (stats.isFile()) return [nextRelative, { kind: "file", mode: stats.mode, hash: await hashFile(fullPath) }];
|
||||
return [nextRelative, null];
|
||||
};
|
||||
const visitBatch = async function* (paths: string[]): AsyncGenerator<[string, SnapshotEntry]> {
|
||||
const results = await Promise.allSettled(paths.map(readEntry));
|
||||
for (const result of results) if (result.status === "rejected") throw result.reason;
|
||||
for (const result of results) {
|
||||
if (result.status !== "fulfilled") continue;
|
||||
const [nextRelative, entry] = result.value;
|
||||
if (!entry) continue;
|
||||
yield [nextRelative, entry];
|
||||
if (entry.kind === "dir") yield* walkDirectory(root, exclude, ignored, nextRelative);
|
||||
}
|
||||
};
|
||||
const current = relative ? path.join(root, relative) : root;
|
||||
let batch: string[] = [];
|
||||
for await (const entry of await fs.opendir(current)) {
|
||||
const nextRelative = relative ? path.posix.join(relative, entry.name) : entry.name;
|
||||
if (shouldExcludePath(nextRelative, exclude) || ignored.matches(nextRelative)) continue;
|
||||
const fullPath = path.join(root, nextRelative);
|
||||
const stats = await fs.lstat(fullPath);
|
||||
if (stats.isDirectory()) {
|
||||
yield [nextRelative, { kind: "dir" }];
|
||||
yield* walkDirectory(root, exclude, ignored, nextRelative);
|
||||
} else if (stats.isSymbolicLink()) {
|
||||
yield [nextRelative, { kind: "symlink", target: await fs.readlink(fullPath) }];
|
||||
} else if (stats.isFile()) {
|
||||
yield [nextRelative, { kind: "file", mode: stats.mode, hash: await hashFile(fullPath) }];
|
||||
batch.push(nextRelative);
|
||||
if (batch.length === 16) {
|
||||
yield* visitBatch(batch);
|
||||
batch = [];
|
||||
}
|
||||
}
|
||||
if (batch.length) yield* visitBatch(batch);
|
||||
}
|
||||
|
||||
async function readSnapshotEntry(root: string, relative: string): Promise<SnapshotEntry | null> {
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { createReadStream, promises as fs } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { setImmediate } from "node:timers/promises";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
captureDirectorySnapshot,
|
||||
disposeDirectorySnapshot,
|
||||
type SnapshotEntry,
|
||||
} from "./workspace-restore-merge.js";
|
||||
|
||||
vi.mock("node:fs", async importOriginal => {
|
||||
const original = await importOriginal<typeof import("node:fs")>();
|
||||
return { ...original, createReadStream: vi.fn(original.createReadStream) };
|
||||
});
|
||||
|
||||
const originalFs = await vi.importActual<typeof import("node:fs")>("node:fs");
|
||||
const roots: string[] = [];
|
||||
|
||||
async function fixture() {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-snapshot-concurrency-"));
|
||||
roots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
vi.mocked(createReadStream).mockImplementation(originalFs.createReadStream);
|
||||
await Promise.all(roots.splice(0).map(root => fs.rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe("bounded workspace snapshot reads", () => {
|
||||
it("preserves file hashes, modes, nested paths, exclusions, and symlink entries across batches", async () => {
|
||||
const root = await fixture();
|
||||
const outside = await fixture();
|
||||
const expected = new Map<string, SnapshotEntry>();
|
||||
for (const directory of ["nested", "nested/deeper"]) {
|
||||
await fs.mkdir(path.join(root, directory));
|
||||
expected.set(directory, { kind: "dir" });
|
||||
}
|
||||
for (let index = 0; index < 73; index++) {
|
||||
const relative = `${index < 35 ? "" : "nested/deeper/"}${index % 3 === 0 ? "line\nbreak" : "-space name"}-${index}`;
|
||||
const content = Buffer.from(`snapshot ${index}\0bytes`);
|
||||
const filePath = path.join(root, relative);
|
||||
await fs.writeFile(filePath, content);
|
||||
await fs.chmod(filePath, index % 2 === 0 ? 0o600 : 0o444);
|
||||
expected.set(relative, {
|
||||
kind: "file", mode: (await fs.lstat(filePath)).mode,
|
||||
hash: createHash("sha256").update(content).digest("hex"),
|
||||
});
|
||||
}
|
||||
await fs.writeFile(path.join(outside, "private"), "outside bytes");
|
||||
await fs.symlink(outside, path.join(root, "outside-link"));
|
||||
expected.set("outside-link", { kind: "symlink", target: outside });
|
||||
for (const ignored of ["excluded", "ignored"]) {
|
||||
await fs.mkdir(path.join(root, ignored));
|
||||
await fs.writeFile(path.join(root, ignored, "private"), "omitted bytes");
|
||||
}
|
||||
for (const diskBacked of [false, true]) {
|
||||
const snapshot = await captureDirectorySnapshot(root, {
|
||||
exclude: ["excluded"], ignoredPaths: ["ignored"], diskBacked,
|
||||
});
|
||||
try {
|
||||
expect(new Map(snapshot.entries)).toEqual(expected);
|
||||
} finally {
|
||||
await disposeDirectorySnapshot(snapshot);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("overlaps reads within a fixed bound even across nested directories", async () => {
|
||||
const root = await fixture();
|
||||
for (const directory of ["", "nested", "nested/deeper"]) {
|
||||
await fs.mkdir(path.join(root, directory), { recursive: true });
|
||||
await Promise.all(Array.from({ length: 35 }, (_, index) =>
|
||||
fs.writeFile(path.join(root, directory, `file-${index}`), String(index))));
|
||||
}
|
||||
let active = 0;
|
||||
let peak = 0;
|
||||
vi.mocked(createReadStream).mockImplementation((...args) => {
|
||||
const stream = originalFs.createReadStream(...args);
|
||||
stream.pause();
|
||||
active++;
|
||||
peak = Math.max(peak, active);
|
||||
const settled = () => { active--; };
|
||||
stream.once("end", settled);
|
||||
stream.once("error", settled);
|
||||
setTimeout(() => stream.resume(), 10);
|
||||
return stream;
|
||||
});
|
||||
const snapshot = await captureDirectorySnapshot(root, { diskBacked: true });
|
||||
try {
|
||||
expect(snapshot.entries.size).toBe(107);
|
||||
expect(peak).toBeGreaterThan(1);
|
||||
expect(peak).toBeLessThanOrEqual(16);
|
||||
expect(active).toBe(0);
|
||||
} finally {
|
||||
await disposeDirectorySnapshot(snapshot);
|
||||
}
|
||||
});
|
||||
|
||||
it("settles started metadata reads before propagating a peer failure", async () => {
|
||||
const root = await fixture();
|
||||
await fs.writeFile(path.join(root, "denied"), "denied");
|
||||
await fs.writeFile(path.join(root, "pending"), "pending");
|
||||
const lstat = fs.lstat.bind(fs);
|
||||
let release!: () => void;
|
||||
const gate = new Promise<void>(resolve => { release = resolve; });
|
||||
let pending = false;
|
||||
let active = 0;
|
||||
vi.spyOn(fs, "lstat").mockImplementation(async (...args: Parameters<typeof fs.lstat>) => {
|
||||
if (String(args[0]) === path.join(root, "denied")) throw Object.assign(new Error("denied"), { code: "EACCES" });
|
||||
active++;
|
||||
pending = true;
|
||||
try {
|
||||
await gate;
|
||||
return await lstat(...args);
|
||||
} finally {
|
||||
active--;
|
||||
}
|
||||
});
|
||||
let settled = false;
|
||||
const result = captureDirectorySnapshot(root, { diskBacked: true }).then(
|
||||
() => { settled = true; return null; },
|
||||
error => { settled = true; return error; },
|
||||
);
|
||||
try {
|
||||
await vi.waitFor(() => expect(pending).toBe(true));
|
||||
expect(settled).toBe(false);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
expect(await result).toMatchObject({ code: "EACCES" });
|
||||
expect(active).toBe(0);
|
||||
});
|
||||
|
||||
it("settles delayed peer content reads before returning a content failure", async () => {
|
||||
const root = await fixture();
|
||||
await fs.writeFile(path.join(root, "failed"), "failed");
|
||||
await fs.writeFile(path.join(root, "pending"), "pending bytes");
|
||||
let release!: () => void;
|
||||
const gate = new Promise<void>(resolve => { release = resolve; });
|
||||
let pending = false;
|
||||
let failed = false;
|
||||
let active = 0;
|
||||
vi.mocked(createReadStream).mockImplementation((...args) => {
|
||||
const stream = originalFs.createReadStream(...args);
|
||||
stream.pause();
|
||||
active++;
|
||||
stream.once("end", () => { active--; });
|
||||
stream.once("error", () => { active--; failed = true; });
|
||||
if (String(args[0]) === path.join(root, "failed")) {
|
||||
void setImmediate().then(() => stream.destroy(Object.assign(new Error("read failed"), { code: "EIO" })));
|
||||
} else {
|
||||
pending = true;
|
||||
void gate.then(() => stream.resume());
|
||||
}
|
||||
return stream;
|
||||
});
|
||||
let settled = false;
|
||||
const result = captureDirectorySnapshot(root, { diskBacked: true }).then(
|
||||
() => { settled = true; return null; },
|
||||
error => { settled = true; return error; },
|
||||
);
|
||||
try {
|
||||
await vi.waitFor(() => expect(failed && pending).toBe(true));
|
||||
expect(settled).toBe(false);
|
||||
expect(active).toBe(1);
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
expect(await result).toMatchObject({ code: "EIO" });
|
||||
expect(active).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,6 @@ export const SANDBOX_INSTALL_COMMAND = "npm install -g @openai/codex";
|
||||
export const DEFAULT_CODEX_LOCAL_MODEL = PAPERCLIP_RUNNER_DEFAULT_MODELS.codex;
|
||||
export const DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX = true;
|
||||
|
||||
/** Shared by CLI execution and configuration previews; explicit arguments keep their policy. */
|
||||
export function resolveCodexLocalCliPolicy(config: unknown, options: { networkAccess?: boolean } = {}) {
|
||||
const record = typeof config === "object" && config !== null && !Array.isArray(config)
|
||||
? config as Record<string, unknown> : {};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createProviderStoppedBoundary } from "@paperclipai/adapter-utils/provider-stopped-boundary";
|
||||
import { createUsageCheckpointLog } from "@paperclipai/adapter-utils/usage-checkpoint";
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -9,6 +10,7 @@ import { copyBackCodexAuth } from "./codex-auth-copyback.js";
|
||||
import {
|
||||
ensureCodexAuthCacheEntryDir,
|
||||
isCodexAuthCacheEnabled,
|
||||
readSubscriptionAccountId,
|
||||
resolveCodexAuthCacheEntryPath,
|
||||
selectVendCredential,
|
||||
} from "./codex-auth-cache.js";
|
||||
@@ -642,27 +644,50 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
typeof envConfig.CODEX_HOME === "string" && envConfig.CODEX_HOME.trim().length > 0
|
||||
? path.resolve(envConfig.CODEX_HOME.trim())
|
||||
: null;
|
||||
const connectorSourceHome = configuredCodexHome;
|
||||
const connectorSkillDigest = typeof config.paperclipConnectorSkillDigest === "string"
|
||||
&& /^[a-f0-9]{64}$/.test(config.paperclipConnectorSkillDigest) ? config.paperclipConnectorSkillDigest : null;
|
||||
if (connectorSkillDigest) {
|
||||
// Never mount assignment-specific skills into the shared company/user home.
|
||||
// A different skill revision gets a new home, so revoked/changed resources
|
||||
// cannot survive as stale symlinks or bleed into another agent's session.
|
||||
configuredCodexHome = path.join(resolveManagedCodexHomeDir(process.env, agent.companyId),
|
||||
"connector-runtimes", agent.id, connectorSkillDigest);
|
||||
}
|
||||
const codexSkillEntries = (await readPaperclipRuntimeSkillEntries(config, __moduleDir))
|
||||
// A missing-source entry would become a dangling skill symlink; skip it.
|
||||
.filter((entry) => !isPaperclipSkillSourceMissing(entry));
|
||||
const desiredSkillNames = resolveCodexDesiredSkillNames(config, codexSkillEntries);
|
||||
if (!executionTargetIsRemote) {
|
||||
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||
}
|
||||
const configuredOpenAiApiKey =
|
||||
typeof envConfig.OPENAI_API_KEY === "string" && envConfig.OPENAI_API_KEY.trim().length > 0
|
||||
? envConfig.OPENAI_API_KEY.trim()
|
||||
: null;
|
||||
const skillSourceHome = configuredCodexHome;
|
||||
const connectorSkillDigest = typeof config.paperclipConnectorSkillDigest === "string"
|
||||
&& /^[a-f0-9]{64}$/.test(config.paperclipConnectorSkillDigest) ? config.paperclipConnectorSkillDigest : null;
|
||||
const codexSkillEntries = (await readPaperclipRuntimeSkillEntries(config, __moduleDir))
|
||||
.filter((entry) => !isPaperclipSkillSourceMissing(entry));
|
||||
const desiredSkillNames = resolveCodexDesiredSkillNames(config, codexSkillEntries);
|
||||
const desiredSet = new Set(desiredSkillNames);
|
||||
// An explicit inventory is the server's authorized execution selection. Include
|
||||
// empty inventories: revocation must not fall back to a previously populated home.
|
||||
let runtimeSkillDigest = Array.isArray(config.paperclipRuntimeSkills)
|
||||
? createHash("sha256").update(JSON.stringify({ connectorSkillDigest,
|
||||
skills: codexSkillEntries.filter(entry => desiredSet.has(entry.key))
|
||||
.map(entry => [entry.key, entry.source]).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
|
||||
})).digest("hex")
|
||||
: connectorSkillDigest;
|
||||
const runtimeCredentialHome = runtimeSkillDigest
|
||||
? skillSourceHome ?? await prepareManagedCodexHome(process.env, onLog, agent.companyId, { apiKey: configuredOpenAiApiKey })
|
||||
: null;
|
||||
if (runtimeSkillDigest) {
|
||||
const credentialHome = runtimeCredentialHome!;
|
||||
const credentialBytes = await fs.readFile(path.join(credentialHome, "auth.json")).catch(() => null);
|
||||
const selectedConfig = await fs.readFile(path.join(credentialHome, "config.toml")).catch(() => null);
|
||||
// Keep selected-home provider/MCP edits (and removals) in the launched home.
|
||||
const selectedConfigDigest = selectedConfig ? createHash("sha256").update(selectedConfig).digest("hex") : null;
|
||||
// Copy-back may retain a refreshed regular credential in a generated home.
|
||||
// Account switches must select a different home, while token refreshes for
|
||||
// the same subscription identity preserve its cache and session state.
|
||||
const credentialIdentity = credentialBytes
|
||||
? readSubscriptionAccountId(credentialBytes) ?? createHash("sha256").update(credentialBytes).digest("hex") : null;
|
||||
runtimeSkillDigest = createHash("sha256").update(JSON.stringify({
|
||||
runtimeSkillDigest, credentialHome, credentialIdentity, selectedConfigDigest, apiKey: envConfig.OPENAI_API_KEY ?? null,
|
||||
})).digest("hex");
|
||||
// Different authorized versions/assignments get separate homes, including
|
||||
// session state. Previously permitted private links cannot survive revocation.
|
||||
configuredCodexHome = path.join(resolveManagedCodexHomeDir(process.env, agent.companyId),
|
||||
connectorSkillDigest ? "connector-runtimes" : "skill-runtimes", agent.id, runtimeSkillDigest);
|
||||
}
|
||||
if (!executionTargetIsRemote) {
|
||||
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||
}
|
||||
// A configured CODEX_HOME that lives under the Paperclip-managed company tree
|
||||
// (the per-agent home set by the server isolation guard) still needs auth
|
||||
// seeded — it ships with no credentials and OPENAI_API_KEY="" by default.
|
||||
@@ -697,14 +722,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
void error;
|
||||
});
|
||||
}
|
||||
if (configuredCodexHome == null || (connectorSkillDigest && connectorSourceHome == null)) {
|
||||
if (configuredCodexHome == null) {
|
||||
await prepareManagedCodexHome(process.env, onLog, agent.companyId, {
|
||||
apiKey: configuredOpenAiApiKey,
|
||||
});
|
||||
}
|
||||
if (configuredHomeIsManaged && configuredCodexHome) {
|
||||
const seedEnv = connectorSkillDigest ? {
|
||||
...process.env, CODEX_HOME: connectorSourceHome ?? resolveManagedCodexHomeDir(process.env, agent.companyId),
|
||||
const seedEnv = runtimeSkillDigest ? {
|
||||
...process.env, CODEX_HOME: runtimeCredentialHome ?? resolveManagedCodexHomeDir(process.env, agent.companyId),
|
||||
} : process.env;
|
||||
await seedManagedCodexHome(configuredCodexHome, seedEnv, onLog, {
|
||||
apiKey: configuredOpenAiApiKey,
|
||||
@@ -737,7 +762,8 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
// Merge custom model providers (PAPERCLIP_CODEX_PROVIDERS) into the managed
|
||||
// CODEX_HOME's config.toml BEFORE the home is shipped to a remote execution
|
||||
// target, so both local and sandboxed Codex processes pick up the routing.
|
||||
// An explicit env.CODEX_HOME override is treated as user-managed and skipped.
|
||||
// A caller-owned home stays untouched. A generated skill home receives the
|
||||
// configured provider routing even when it seeds credentials from that home.
|
||||
const envConfigStrings = Object.fromEntries(
|
||||
Object.entries(envConfig).filter(
|
||||
(entry): entry is [string, string] => typeof entry[1] === "string",
|
||||
@@ -745,7 +771,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
);
|
||||
const preparedRuntimeConfig = await prepareCodexRuntimeConfig({
|
||||
env: envConfigStrings,
|
||||
codexHome: configuredCodexHome ? null : effectiveCodexHome,
|
||||
codexHome: configuredCodexHome && !runtimeSkillDigest ? null : effectiveCodexHome,
|
||||
});
|
||||
// Curated allowlist dir staged for the remote `home` asset (see below). Held
|
||||
// here so the outer `finally` can remove it on every exit path (teardown and
|
||||
|
||||
@@ -473,6 +473,7 @@ const MIGRATIONS_WITH_TOP_LEVEL_STATEMENTS = new Set([
|
||||
"0313_private_task_access.sql",
|
||||
"0314_private_task_draft_assets.sql",
|
||||
"0332_little_night_nurse.sql",
|
||||
"0345_luxuriant_carmella_unuscione.sql",
|
||||
]);
|
||||
|
||||
async function applyPendingMigrationsManually(
|
||||
|
||||
@@ -27,6 +27,8 @@ d("heartbeat context_snapshot expression index migration", () => {
|
||||
expect(names).toContain("heartbeat_runs_company_ctx_issue_created_idx");
|
||||
expect(names).toContain("heartbeat_runs_company_ctx_task_created_idx");
|
||||
expect(names).toContain("heartbeat_runs_company_ctx_taskkey_created_idx");
|
||||
expect(names).toContain("heartbeat_runs_company_ctx_paperclip_issue_created_idx");
|
||||
expect(names).toContain("heartbeat_runs_company_origin_issue_idx");
|
||||
expect(names).toContain("agent_wakeup_requests_company_payload_issue_idx");
|
||||
|
||||
await sql.unsafe("SET enable_seqscan = off");
|
||||
@@ -52,6 +54,20 @@ d("heartbeat context_snapshot expression index migration", () => {
|
||||
const taskKeyText = taskKeyPlan.map((r) => Object.values(r)[0]).join("\n");
|
||||
expect(taskKeyText).toContain("heartbeat_runs_company_ctx_taskkey_created_idx");
|
||||
|
||||
// An empty table can use any company-prefix index for the OR. Check the
|
||||
// new branch directly here; the populated benchmark proves BitmapOr use.
|
||||
const redactionPlan = await sql.unsafe(
|
||||
"EXPLAIN SELECT id FROM heartbeat_runs WHERE company_id = '00000000-0000-0000-0000-000000000001' AND context_snapshot -> 'paperclipIssue' ->> 'id' = 'x'",
|
||||
);
|
||||
const redactionText = redactionPlan.map((r) => Object.values(r)[0]).join("\n");
|
||||
expect(redactionText).toContain("heartbeat_runs_company_ctx_paperclip_issue_created_idx");
|
||||
|
||||
const originPlan = await sql.unsafe(
|
||||
"EXPLAIN SELECT id FROM heartbeat_runs WHERE company_id = '00000000-0000-0000-0000-000000000001' AND coalesce(native_issue_id::text, nullif(context_snapshot->>'issueId', ''), nullif(context_snapshot->>'taskId', ''), nullif(context_snapshot->>'taskKey', '')) IN ('x', 'SRC-1')",
|
||||
);
|
||||
expect(originPlan.map((r) => Object.values(r)[0]).join("\n"))
|
||||
.toContain("heartbeat_runs_company_origin_issue_idx");
|
||||
|
||||
const wakePlan = await sql.unsafe(
|
||||
"EXPLAIN SELECT id FROM agent_wakeup_requests WHERE company_id = '00000000-0000-0000-0000-000000000001' AND status = 'deferred_issue_execution' AND payload ->> 'issueId' = 'x' LIMIT 1",
|
||||
);
|
||||
@@ -63,6 +79,7 @@ d("heartbeat context_snapshot expression index migration", () => {
|
||||
for (const migration of [
|
||||
"./migrations/0209_heartbeat_context_snapshot_indexes.sql",
|
||||
"./migrations/0210_heartbeat_context_taskkey_index.sql",
|
||||
"./migrations/0345_luxuriant_carmella_unuscione.sql",
|
||||
]) {
|
||||
const migrationSql = await readFile(
|
||||
fileURLToPath(new URL(migration, import.meta.url)),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
ALTER TABLE "company_secrets" DROP CONSTRAINT "company_secrets_scope_shape_check";--> statement-breakpoint
|
||||
ALTER TABLE "company_secrets" ADD COLUMN "owner_agent_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "company_secrets" ADD COLUMN "owner_plugin_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "company_secrets" ADD CONSTRAINT "company_secrets_owner_plugin_id_plugins_id_fk" FOREIGN KEY ("owner_plugin_id") REFERENCES "public"."plugins"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "company_secrets" ADD CONSTRAINT "company_secrets_company_id_owner_agent_id_agents_company_id_id_fk" FOREIGN KEY ("company_id","owner_agent_id") REFERENCES "public"."agents"("company_id","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "company_secrets_instance_owner_key_uq" ON "company_secrets" USING btree ("company_id","owner_agent_id","owner_plugin_id","key") WHERE "company_secrets"."scope" = 'instance' and "company_secrets"."deleted_at" is null;--> statement-breakpoint
|
||||
ALTER TABLE "company_secrets" ADD CONSTRAINT "company_secrets_scope_shape_check" CHECK ((
|
||||
"company_secrets"."scope" = 'company'
|
||||
and "company_secrets"."owner_user_id" is null
|
||||
and "company_secrets"."user_secret_definition_id" is null
|
||||
and "company_secrets"."owner_agent_id" is null and "company_secrets"."owner_plugin_id" is null
|
||||
) or (
|
||||
"company_secrets"."scope" = 'user'
|
||||
and "company_secrets"."owner_user_id" is not null
|
||||
and "company_secrets"."user_secret_definition_id" is not null
|
||||
and "company_secrets"."owner_agent_id" is null and "company_secrets"."owner_plugin_id" is null
|
||||
) or (
|
||||
"company_secrets"."scope" = 'instance'
|
||||
and "company_secrets"."owner_user_id" is null and "company_secrets"."user_secret_definition_id" is null
|
||||
and "company_secrets"."owner_agent_id" is not null and "company_secrets"."owner_plugin_id" is not null
|
||||
));
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "resource_lifecycle_events" DROP CONSTRAINT "resource_lifecycle_events_action_check";--> statement-breakpoint
|
||||
ALTER TABLE "resource_lifecycle_events" ADD CONSTRAINT "resource_lifecycle_events_action_check" CHECK ("resource_lifecycle_events"."action" = 'create' OR ("resource_lifecycle_events"."resource_type" = 'project' AND "resource_lifecycle_events"."action" IN ('update', 'archive', 'delete')) OR ("resource_lifecycle_events"."resource_type" = 'agent' AND "resource_lifecycle_events"."action" IN ('pause', 'resume', 'terminate')));
|
||||
@@ -0,0 +1,78 @@
|
||||
-- Replay-safe after renumbering: preserve existing imports, provenance and scheduling.
|
||||
CREATE TABLE IF NOT EXISTS "project_skill_sources" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"project_id" uuid NOT NULL,
|
||||
"workspace_id" uuid,
|
||||
"source_id" uuid,
|
||||
"repository_url" text NOT NULL,
|
||||
"tracking_ref" text NOT NULL,
|
||||
"active" boolean DEFAULT true NOT NULL,
|
||||
"status" text DEFAULT 'pending' NOT NULL,
|
||||
"refresh_user_id" text,
|
||||
"refresh_agent_id" uuid,
|
||||
"refresh_local_trusted" boolean DEFAULT false NOT NULL,
|
||||
"connection_id" uuid,
|
||||
"refresh_grant_id" uuid,
|
||||
"next_sync_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"failure_count" integer DEFAULT 0 NOT NULL,
|
||||
"last_error" text,
|
||||
"lease_token" uuid,
|
||||
"lease_expires_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" DROP CONSTRAINT IF EXISTS "company_skill_sources_connection_id_tool_connections_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "automatic_updates" boolean DEFAULT true NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "import_new_skills" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "access_scope" text DEFAULT 'company' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "refresh_user_id" text;--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "refresh_agent_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "refresh_local_trusted" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "refresh_grant_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "next_sync_at" timestamp with time zone DEFAULT now() NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "failure_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD COLUMN IF NOT EXISTS "access_source_id" uuid;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'project_skill_sources_company_id_companies_id_fk' AND conrelid = 'public.project_skill_sources'::regclass) THEN
|
||||
ALTER TABLE "project_skill_sources" ADD CONSTRAINT "project_skill_sources_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'project_skill_sources_project_id_projects_id_fk' AND conrelid = 'public.project_skill_sources'::regclass) THEN
|
||||
ALTER TABLE "project_skill_sources" ADD CONSTRAINT "project_skill_sources_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'project_skill_sources_workspace_id_project_workspaces_id_fk' AND conrelid = 'public.project_skill_sources'::regclass) THEN
|
||||
ALTER TABLE "project_skill_sources" ADD CONSTRAINT "project_skill_sources_workspace_id_project_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."project_workspaces"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'project_skill_sources_source_id_company_skill_sources_id_fk' AND conrelid = 'public.project_skill_sources'::regclass) THEN
|
||||
ALTER TABLE "project_skill_sources" ADD CONSTRAINT "project_skill_sources_source_id_company_skill_sources_id_fk" FOREIGN KEY ("source_id") REFERENCES "public"."company_skill_sources"("id") ON DELETE set null ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "project_skill_sources_workspace_repo_ref_idx" ON "project_skill_sources" USING btree ("workspace_id","repository_url","tracking_ref");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "project_skill_sources_source_project_idx" ON "project_skill_sources" USING btree ("company_id","source_id","project_id");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "project_skill_sources_sync_idx" ON "project_skill_sources" USING btree ("active","status","next_sync_at");
|
||||
--> statement-breakpoint
|
||||
-- Retain setup identity if a connection or agent is deleted, including preview schemas.
|
||||
ALTER TABLE "company_skill_sources" DROP CONSTRAINT IF EXISTS "company_skill_sources_refresh_agent_id_agents_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "project_skill_sources" DROP CONSTRAINT IF EXISTS "project_skill_sources_refresh_agent_id_agents_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "project_skill_sources" DROP CONSTRAINT IF EXISTS "project_skill_sources_connection_id_tool_connections_id_fk";
|
||||
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "removed_at" timestamp with time zone;
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "company_skill_sources_repository_ref_idx";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "company_skill_sources_identity_ref_idx";--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "company_skill_sources_repository_ref_idx" ON "company_skill_sources" USING btree ("company_id","repository_url","tracking_ref") WHERE "company_skill_sources"."removed_at" is null;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "company_skill_sources_identity_ref_idx" ON "company_skill_sources" USING btree ("company_id","repository_id","tracking_ref") WHERE "company_skill_sources"."removed_at" is null;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "last_scan_tree_sha" text;--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "automatic_budget_bytes" integer DEFAULT 536870912 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_sources" ADD COLUMN IF NOT EXISTS "automatic_budget_at" timestamp with time zone DEFAULT now() NOT NULL;
|
||||
@@ -0,0 +1,7 @@
|
||||
CREATE TABLE "company_skill_import_budgets" (
|
||||
"company_id" uuid PRIMARY KEY NOT NULL,
|
||||
"available_bytes" integer DEFAULT 536870912 NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "company_skill_import_budgets" ADD CONSTRAINT "company_skill_import_budgets_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
|
||||
@@ -0,0 +1,14 @@
|
||||
ALTER TABLE "agents" ADD COLUMN IF NOT EXISTS "created_by_user_id" text;
|
||||
--> statement-breakpoint
|
||||
UPDATE "agents" SET "created_by_user_id" = (
|
||||
SELECT "activity_log"."actor_id"
|
||||
FROM "activity_log"
|
||||
WHERE "activity_log"."entity_type" = 'agent'
|
||||
AND "activity_log"."entity_id" = "agents"."id"::text
|
||||
AND "activity_log"."company_id" = "agents"."company_id"
|
||||
AND "activity_log"."action" IN ('agent.created', 'agent.hire_created')
|
||||
AND "activity_log"."actor_type" = 'user'
|
||||
ORDER BY "activity_log"."created_at" ASC
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE "created_by_user_id" IS NULL;
|
||||
@@ -0,0 +1,62 @@
|
||||
CREATE TABLE "grok_bot_dispatches" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"routine_id" uuid NOT NULL,
|
||||
"binding_id" uuid NOT NULL,
|
||||
"binding_generation" integer NOT NULL,
|
||||
"grant_id" uuid NOT NULL,
|
||||
"routine_revision" integer NOT NULL,
|
||||
"assignment_id" uuid,
|
||||
"source_event_id" text NOT NULL,
|
||||
"kind" text NOT NULL,
|
||||
"payload" jsonb NOT NULL,
|
||||
"challenge_hash" text,
|
||||
"status" text DEFAULT 'prepared' NOT NULL,
|
||||
"http_status" integer,
|
||||
"expires_at" timestamp with time zone NOT NULL,
|
||||
"sent_at" timestamp with time zone,
|
||||
"acknowledged_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "grok_bot_routines" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"agent_id" uuid NOT NULL,
|
||||
"binding_id" uuid NOT NULL,
|
||||
"binding_generation" integer NOT NULL,
|
||||
"bot_name" text NOT NULL,
|
||||
"secret_id" uuid NOT NULL,
|
||||
"secret_version" integer NOT NULL,
|
||||
"revision" integer DEFAULT 1 NOT NULL,
|
||||
"verified_grant_id" uuid,
|
||||
"verified_at" timestamp with time zone,
|
||||
"last_contact_at" timestamp with time zone,
|
||||
"recovery_required_at" timestamp with time zone,
|
||||
"recovery_reason" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "dot_agent_bindings" ADD COLUMN "provider_kind" text DEFAULT 'openai_dot' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "dot_runner_operations" ADD COLUMN "pending_returned_at" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "dot_runner_operations" ADD COLUMN "observed_at" timestamp with time zone;--> statement-breakpoint
|
||||
ALTER TABLE "mcp_attachment_uploads" ADD COLUMN "grant_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "mcp_attachment_uploads" ADD COLUMN "work_session_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "mcp_file_tickets" ADD COLUMN "work_session_id" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "grok_bot_dispatches" ADD CONSTRAINT "grok_bot_dispatches_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "grok_bot_dispatches" ADD CONSTRAINT "grok_bot_dispatches_routine_id_grok_bot_routines_id_fk" FOREIGN KEY ("routine_id") REFERENCES "public"."grok_bot_routines"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "grok_bot_dispatches" ADD CONSTRAINT "grok_bot_dispatches_binding_id_dot_agent_bindings_id_fk" FOREIGN KEY ("binding_id") REFERENCES "public"."dot_agent_bindings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "grok_bot_dispatches" ADD CONSTRAINT "grok_bot_dispatches_grant_id_mcp_oauth_grants_id_fk" FOREIGN KEY ("grant_id") REFERENCES "public"."mcp_oauth_grants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "grok_bot_dispatches" ADD CONSTRAINT "grok_bot_dispatches_assignment_id_dot_runner_assignments_id_fk" FOREIGN KEY ("assignment_id") REFERENCES "public"."dot_runner_assignments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "grok_bot_routines" ADD CONSTRAINT "grok_bot_routines_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "grok_bot_routines" ADD CONSTRAINT "grok_bot_routines_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "grok_bot_routines" ADD CONSTRAINT "grok_bot_routines_binding_id_dot_agent_bindings_id_fk" FOREIGN KEY ("binding_id") REFERENCES "public"."dot_agent_bindings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "grok_bot_routines" ADD CONSTRAINT "grok_bot_routines_secret_id_company_secrets_id_fk" FOREIGN KEY ("secret_id") REFERENCES "public"."company_secrets"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "grok_bot_routines" ADD CONSTRAINT "grok_bot_routines_verified_grant_id_mcp_oauth_grants_id_fk" FOREIGN KEY ("verified_grant_id") REFERENCES "public"."mcp_oauth_grants"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "grok_dispatch_event_uq" ON "grok_bot_dispatches" USING btree ("binding_id","binding_generation","routine_revision","source_event_id");--> statement-breakpoint
|
||||
CREATE INDEX "grok_dispatch_company_status_idx" ON "grok_bot_dispatches" USING btree ("company_id","status");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "grok_routines_binding_uq" ON "grok_bot_routines" USING btree ("binding_id");--> statement-breakpoint
|
||||
CREATE INDEX "grok_routines_company_agent_idx" ON "grok_bot_routines" USING btree ("company_id","agent_id");--> statement-breakpoint
|
||||
ALTER TABLE "mcp_attachment_uploads" ADD CONSTRAINT "mcp_attachment_uploads_grant_id_mcp_oauth_grants_id_fk" FOREIGN KEY ("grant_id") REFERENCES "public"."mcp_oauth_grants"("id") ON DELETE cascade ON UPDATE no action;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Keep both JSON branches indexable without blocking run writes during the build.
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS "heartbeat_runs_company_ctx_paperclip_issue_created_idx" ON "heartbeat_runs" USING btree ("company_id",("context_snapshot" -> 'paperclipIssue' ->> 'id'),"created_at" DESC NULLS LAST);--> statement-breakpoint
|
||||
-- Match the creation-source query's exact native/legacy precedence.
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS "heartbeat_runs_company_origin_issue_idx" ON "heartbeat_runs" USING btree ("company_id",coalesce(
|
||||
"native_issue_id"::text,
|
||||
nullif("context_snapshot"->>'issueId', ''),
|
||||
nullif("context_snapshot"->>'taskId', ''),
|
||||
nullif("context_snapshot"->>'taskKey', '')
|
||||
));
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Find an assignment's work wakes without scanning the company's dispatch history.
|
||||
CREATE INDEX "grok_dispatch_assignment_kind_idx" ON "grok_bot_dispatches" USING btree ("assignment_id","kind");
|
||||
-58685
File diff suppressed because it is too large.
Load diff
-58707
File diff suppressed because it is too large.
Load diff
-58763
File diff suppressed because it is too large.
Load diff
-58882
File diff suppressed because it is too large.
Load diff
+486
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "108e07e7-19ab-4f16-8400-fd733f77afdc",
|
||||
"prevId": "65f48b10-7f94-449e-b2ce-a68f220910f3",
|
||||
"id": "e9a4bbc2-b22c-48a1-a74c-f108cd7b20ec",
|
||||
"prevId": "927d7532-059b-4a4f-846a-5e3445a81891",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
@@ -16324,6 +16324,18 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"owner_agent_id": {
|
||||
"name": "owner_agent_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"owner_plugin_id": {
|
||||
"name": "owner_plugin_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_secret_definition_id": {
|
||||
"name": "user_secret_definition_id",
|
||||
"type": "uuid",
|
||||
@@ -16552,6 +16564,40 @@
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"company_secrets_instance_owner_key_uq": {
|
||||
"name": "company_secrets_instance_owner_key_uq",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "owner_agent_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "owner_plugin_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "key",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"where": "\"company_secrets\".\"scope\" = 'instance' and \"company_secrets\".\"deleted_at\" is null",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"company_secrets_provider_config_idx": {
|
||||
"name": "company_secrets_provider_config_idx",
|
||||
"columns": [
|
||||
@@ -16654,6 +16700,19 @@
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"company_secrets_owner_plugin_id_plugins_id_fk": {
|
||||
"name": "company_secrets_owner_plugin_id_plugins_id_fk",
|
||||
"tableFrom": "company_secrets",
|
||||
"tableTo": "plugins",
|
||||
"columnsFrom": [
|
||||
"owner_plugin_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"company_secrets_user_secret_definition_id_user_secret_definitions_id_fk": {
|
||||
"name": "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk",
|
||||
"tableFrom": "company_secrets",
|
||||
@@ -16692,6 +16751,21 @@
|
||||
],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"company_secrets_company_id_owner_agent_id_agents_company_id_id_fk": {
|
||||
"name": "company_secrets_company_id_owner_agent_id_agents_company_id_id_fk",
|
||||
"tableFrom": "company_secrets",
|
||||
"tableTo": "agents",
|
||||
"columnsFrom": [
|
||||
"company_id",
|
||||
"owner_agent_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"company_id",
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
@@ -16700,7 +16774,7 @@
|
||||
"checkConstraints": {
|
||||
"company_secrets_scope_shape_check": {
|
||||
"name": "company_secrets_scope_shape_check",
|
||||
"value": "(\n \"company_secrets\".\"scope\" = 'company'\n and \"company_secrets\".\"owner_user_id\" is null\n and \"company_secrets\".\"user_secret_definition_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'user'\n and \"company_secrets\".\"owner_user_id\" is not null\n and \"company_secrets\".\"user_secret_definition_id\" is not null\n )"
|
||||
"value": "(\n \"company_secrets\".\"scope\" = 'company'\n and \"company_secrets\".\"owner_user_id\" is null\n and \"company_secrets\".\"user_secret_definition_id\" is null\n and \"company_secrets\".\"owner_agent_id\" is null and \"company_secrets\".\"owner_plugin_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'user'\n and \"company_secrets\".\"owner_user_id\" is not null\n and \"company_secrets\".\"user_secret_definition_id\" is not null\n and \"company_secrets\".\"owner_agent_id\" is null and \"company_secrets\".\"owner_plugin_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'instance'\n and \"company_secrets\".\"owner_user_id\" is null and \"company_secrets\".\"user_secret_definition_id\" is null\n and \"company_secrets\".\"owner_agent_id\" is not null and \"company_secrets\".\"owner_plugin_id\" is not null\n )"
|
||||
}
|
||||
},
|
||||
"isRLSEnabled": false
|
||||
@@ -16778,6 +16852,53 @@
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.company_skill_import_budgets": {
|
||||
"name": "company_skill_import_budgets",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"available_bytes": {
|
||||
"name": "available_bytes",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 536870912
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"company_skill_import_budgets_company_id_companies_id_fk": {
|
||||
"name": "company_skill_import_budgets_company_id_companies_id_fk",
|
||||
"tableFrom": "company_skill_import_budgets",
|
||||
"tableTo": "companies",
|
||||
"columnsFrom": [
|
||||
"company_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.company_skill_source_entries": {
|
||||
"name": "company_skill_source_entries",
|
||||
"schema": "",
|
||||
@@ -17017,6 +17138,72 @@
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"removed_at": {
|
||||
"name": "removed_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"automatic_updates": {
|
||||
"name": "automatic_updates",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"import_new_skills": {
|
||||
"name": "import_new_skills",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"access_scope": {
|
||||
"name": "access_scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'company'"
|
||||
},
|
||||
"refresh_user_id": {
|
||||
"name": "refresh_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_agent_id": {
|
||||
"name": "refresh_agent_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_local_trusted": {
|
||||
"name": "refresh_local_trusted",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"refresh_grant_id": {
|
||||
"name": "refresh_grant_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"next_sync_at": {
|
||||
"name": "next_sync_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"failure_count": {
|
||||
"name": "failure_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"revision": {
|
||||
"name": "revision",
|
||||
"type": "integer",
|
||||
@@ -17042,6 +17229,26 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"last_scan_tree_sha": {
|
||||
"name": "last_scan_tree_sha",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"automatic_budget_bytes": {
|
||||
"name": "automatic_budget_bytes",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 536870912
|
||||
},
|
||||
"automatic_budget_at": {
|
||||
"name": "automatic_budget_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
@@ -17092,6 +17299,7 @@
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"where": "\"company_skill_sources\".\"removed_at\" is null",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
@@ -17119,6 +17327,7 @@
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"where": "\"company_skill_sources\".\"removed_at\" is null",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
@@ -17137,13 +17346,275 @@
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.project_skill_sources": {
|
||||
"name": "project_skill_sources",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"company_skill_sources_connection_id_tool_connections_id_fk": {
|
||||
"name": "company_skill_sources_connection_id_tool_connections_id_fk",
|
||||
"tableFrom": "company_skill_sources",
|
||||
"tableTo": "tool_connections",
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"source_id": {
|
||||
"name": "source_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"repository_url": {
|
||||
"name": "repository_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tracking_ref": {
|
||||
"name": "tracking_ref",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"active": {
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"refresh_user_id": {
|
||||
"name": "refresh_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_agent_id": {
|
||||
"name": "refresh_agent_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_local_trusted": {
|
||||
"name": "refresh_local_trusted",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"connection_id": {
|
||||
"name": "connection_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_grant_id": {
|
||||
"name": "refresh_grant_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"next_sync_at": {
|
||||
"name": "next_sync_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"failure_count": {
|
||||
"name": "failure_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"lease_token": {
|
||||
"name": "lease_token",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"lease_expires_at": {
|
||||
"name": "lease_expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"project_skill_sources_workspace_repo_ref_idx": {
|
||||
"name": "project_skill_sources_workspace_repo_ref_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "workspace_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "repository_url",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "tracking_ref",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"project_skill_sources_source_project_idx": {
|
||||
"name": "project_skill_sources_source_project_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "source_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "project_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"project_skill_sources_sync_idx": {
|
||||
"name": "project_skill_sources_sync_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "active",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "status",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "next_sync_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"project_skill_sources_company_id_companies_id_fk": {
|
||||
"name": "project_skill_sources_company_id_companies_id_fk",
|
||||
"tableFrom": "project_skill_sources",
|
||||
"tableTo": "companies",
|
||||
"columnsFrom": [
|
||||
"connection_id"
|
||||
"company_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"project_skill_sources_project_id_projects_id_fk": {
|
||||
"name": "project_skill_sources_project_id_projects_id_fk",
|
||||
"tableFrom": "project_skill_sources",
|
||||
"tableTo": "projects",
|
||||
"columnsFrom": [
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"project_skill_sources_workspace_id_project_workspaces_id_fk": {
|
||||
"name": "project_skill_sources_workspace_id_project_workspaces_id_fk",
|
||||
"tableFrom": "project_skill_sources",
|
||||
"tableTo": "project_workspaces",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"project_skill_sources_source_id_company_skill_sources_id_fk": {
|
||||
"name": "project_skill_sources_source_id_company_skill_sources_id_fk",
|
||||
"tableFrom": "project_skill_sources",
|
||||
"tableTo": "company_skill_sources",
|
||||
"columnsFrom": [
|
||||
"source_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
@@ -18603,6 +19074,12 @@
|
||||
"notNull": true,
|
||||
"default": "'company'"
|
||||
},
|
||||
"access_source_id": {
|
||||
"name": "access_source_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"public_share_token": {
|
||||
"name": "public_share_token",
|
||||
"type": "text",
|
||||
@@ -46126,7 +46603,7 @@
|
||||
},
|
||||
"resource_lifecycle_events_action_check": {
|
||||
"name": "resource_lifecycle_events_action_check",
|
||||
"value": "\"resource_lifecycle_events\".\"action\" = 'create' OR (\"resource_lifecycle_events\".\"resource_type\" = 'project' AND \"resource_lifecycle_events\".\"action\" IN ('update', 'archive')) OR (\"resource_lifecycle_events\".\"resource_type\" = 'agent' AND \"resource_lifecycle_events\".\"action\" IN ('pause', 'resume', 'terminate'))"
|
||||
"value": "\"resource_lifecycle_events\".\"action\" = 'create' OR (\"resource_lifecycle_events\".\"resource_type\" = 'project' AND \"resource_lifecycle_events\".\"action\" IN ('update', 'archive', 'delete')) OR (\"resource_lifecycle_events\".\"resource_type\" = 'agent' AND \"resource_lifecycle_events\".\"action\" IN ('pause', 'resume', 'terminate'))"
|
||||
}
|
||||
},
|
||||
"isRLSEnabled": false
|
||||
+622
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "65f48b10-7f94-449e-b2ce-a68f220910f3",
|
||||
"prevId": "a9f48522-347d-4290-83dd-07299afb8d03",
|
||||
"id": "b187fa5b-b867-48a2-89b0-e16f71123411",
|
||||
"prevId": "e9a4bbc2-b22c-48a1-a74c-f108cd7b20ec",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
@@ -4098,6 +4098,12 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_by_user_id": {
|
||||
"name": "created_by_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
@@ -16324,6 +16330,18 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"owner_agent_id": {
|
||||
"name": "owner_agent_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"owner_plugin_id": {
|
||||
"name": "owner_plugin_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_secret_definition_id": {
|
||||
"name": "user_secret_definition_id",
|
||||
"type": "uuid",
|
||||
@@ -16552,6 +16570,40 @@
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"company_secrets_instance_owner_key_uq": {
|
||||
"name": "company_secrets_instance_owner_key_uq",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "owner_agent_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "owner_plugin_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "key",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"where": "\"company_secrets\".\"scope\" = 'instance' and \"company_secrets\".\"deleted_at\" is null",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"company_secrets_provider_config_idx": {
|
||||
"name": "company_secrets_provider_config_idx",
|
||||
"columns": [
|
||||
@@ -16654,6 +16706,19 @@
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"company_secrets_owner_plugin_id_plugins_id_fk": {
|
||||
"name": "company_secrets_owner_plugin_id_plugins_id_fk",
|
||||
"tableFrom": "company_secrets",
|
||||
"tableTo": "plugins",
|
||||
"columnsFrom": [
|
||||
"owner_plugin_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"company_secrets_user_secret_definition_id_user_secret_definitions_id_fk": {
|
||||
"name": "company_secrets_user_secret_definition_id_user_secret_definitions_id_fk",
|
||||
"tableFrom": "company_secrets",
|
||||
@@ -16692,6 +16757,21 @@
|
||||
],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"company_secrets_company_id_owner_agent_id_agents_company_id_id_fk": {
|
||||
"name": "company_secrets_company_id_owner_agent_id_agents_company_id_id_fk",
|
||||
"tableFrom": "company_secrets",
|
||||
"tableTo": "agents",
|
||||
"columnsFrom": [
|
||||
"company_id",
|
||||
"owner_agent_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"company_id",
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
@@ -16700,7 +16780,7 @@
|
||||
"checkConstraints": {
|
||||
"company_secrets_scope_shape_check": {
|
||||
"name": "company_secrets_scope_shape_check",
|
||||
"value": "(\n \"company_secrets\".\"scope\" = 'company'\n and \"company_secrets\".\"owner_user_id\" is null\n and \"company_secrets\".\"user_secret_definition_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'user'\n and \"company_secrets\".\"owner_user_id\" is not null\n and \"company_secrets\".\"user_secret_definition_id\" is not null\n )"
|
||||
"value": "(\n \"company_secrets\".\"scope\" = 'company'\n and \"company_secrets\".\"owner_user_id\" is null\n and \"company_secrets\".\"user_secret_definition_id\" is null\n and \"company_secrets\".\"owner_agent_id\" is null and \"company_secrets\".\"owner_plugin_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'user'\n and \"company_secrets\".\"owner_user_id\" is not null\n and \"company_secrets\".\"user_secret_definition_id\" is not null\n and \"company_secrets\".\"owner_agent_id\" is null and \"company_secrets\".\"owner_plugin_id\" is null\n ) or (\n \"company_secrets\".\"scope\" = 'instance'\n and \"company_secrets\".\"owner_user_id\" is null and \"company_secrets\".\"user_secret_definition_id\" is null\n and \"company_secrets\".\"owner_agent_id\" is not null and \"company_secrets\".\"owner_plugin_id\" is not null\n )"
|
||||
}
|
||||
},
|
||||
"isRLSEnabled": false
|
||||
@@ -16778,6 +16858,53 @@
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.company_skill_import_budgets": {
|
||||
"name": "company_skill_import_budgets",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"available_bytes": {
|
||||
"name": "available_bytes",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 536870912
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"company_skill_import_budgets_company_id_companies_id_fk": {
|
||||
"name": "company_skill_import_budgets_company_id_companies_id_fk",
|
||||
"tableFrom": "company_skill_import_budgets",
|
||||
"tableTo": "companies",
|
||||
"columnsFrom": [
|
||||
"company_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.company_skill_source_entries": {
|
||||
"name": "company_skill_source_entries",
|
||||
"schema": "",
|
||||
@@ -17017,6 +17144,72 @@
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"removed_at": {
|
||||
"name": "removed_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"automatic_updates": {
|
||||
"name": "automatic_updates",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"import_new_skills": {
|
||||
"name": "import_new_skills",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"access_scope": {
|
||||
"name": "access_scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'company'"
|
||||
},
|
||||
"refresh_user_id": {
|
||||
"name": "refresh_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_agent_id": {
|
||||
"name": "refresh_agent_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_local_trusted": {
|
||||
"name": "refresh_local_trusted",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"refresh_grant_id": {
|
||||
"name": "refresh_grant_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"next_sync_at": {
|
||||
"name": "next_sync_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"failure_count": {
|
||||
"name": "failure_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"revision": {
|
||||
"name": "revision",
|
||||
"type": "integer",
|
||||
@@ -17042,6 +17235,26 @@
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"last_scan_tree_sha": {
|
||||
"name": "last_scan_tree_sha",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"automatic_budget_bytes": {
|
||||
"name": "automatic_budget_bytes",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 536870912
|
||||
},
|
||||
"automatic_budget_at": {
|
||||
"name": "automatic_budget_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
@@ -17092,6 +17305,7 @@
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"where": "\"company_skill_sources\".\"removed_at\" is null",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
@@ -17119,6 +17333,7 @@
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"where": "\"company_skill_sources\".\"removed_at\" is null",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
@@ -17137,13 +17352,275 @@
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.project_skill_sources": {
|
||||
"name": "project_skill_sources",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"company_skill_sources_connection_id_tool_connections_id_fk": {
|
||||
"name": "company_skill_sources_connection_id_tool_connections_id_fk",
|
||||
"tableFrom": "company_skill_sources",
|
||||
"tableTo": "tool_connections",
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"project_id": {
|
||||
"name": "project_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"source_id": {
|
||||
"name": "source_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"repository_url": {
|
||||
"name": "repository_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tracking_ref": {
|
||||
"name": "tracking_ref",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"active": {
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"refresh_user_id": {
|
||||
"name": "refresh_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_agent_id": {
|
||||
"name": "refresh_agent_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_local_trusted": {
|
||||
"name": "refresh_local_trusted",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"connection_id": {
|
||||
"name": "connection_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_grant_id": {
|
||||
"name": "refresh_grant_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"next_sync_at": {
|
||||
"name": "next_sync_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"failure_count": {
|
||||
"name": "failure_count",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"lease_token": {
|
||||
"name": "lease_token",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"lease_expires_at": {
|
||||
"name": "lease_expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"project_skill_sources_workspace_repo_ref_idx": {
|
||||
"name": "project_skill_sources_workspace_repo_ref_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "workspace_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "repository_url",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "tracking_ref",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"project_skill_sources_source_project_idx": {
|
||||
"name": "project_skill_sources_source_project_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "source_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "project_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"project_skill_sources_sync_idx": {
|
||||
"name": "project_skill_sources_sync_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "active",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "status",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "next_sync_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"project_skill_sources_company_id_companies_id_fk": {
|
||||
"name": "project_skill_sources_company_id_companies_id_fk",
|
||||
"tableFrom": "project_skill_sources",
|
||||
"tableTo": "companies",
|
||||
"columnsFrom": [
|
||||
"connection_id"
|
||||
"company_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"project_skill_sources_project_id_projects_id_fk": {
|
||||
"name": "project_skill_sources_project_id_projects_id_fk",
|
||||
"tableFrom": "project_skill_sources",
|
||||
"tableTo": "projects",
|
||||
"columnsFrom": [
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"project_skill_sources_workspace_id_project_workspaces_id_fk": {
|
||||
"name": "project_skill_sources_workspace_id_project_workspaces_id_fk",
|
||||
"tableFrom": "project_skill_sources",
|
||||
"tableTo": "project_workspaces",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"project_skill_sources_source_id_company_skill_sources_id_fk": {
|
||||
"name": "project_skill_sources_source_id_company_skill_sources_id_fk",
|
||||
"tableFrom": "project_skill_sources",
|
||||
"tableTo": "company_skill_sources",
|
||||
"columnsFrom": [
|
||||
"source_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
@@ -18603,6 +19080,12 @@
|
||||
"notNull": true,
|
||||
"default": "'company'"
|
||||
},
|
||||
"access_source_id": {
|
||||
"name": "access_source_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"public_share_token": {
|
||||
"name": "public_share_token",
|
||||
"type": "text",
|
||||
@@ -26839,6 +27322,136 @@
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.execution_workspace_repository_access": {
|
||||
"name": "execution_workspace_repository_access",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"company_id": {
|
||||
"name": "company_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"root_key": {
|
||||
"name": "root_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"canonical_cwd": {
|
||||
"name": "canonical_cwd",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"kind": {
|
||||
"name": "kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"scope_id": {
|
||||
"name": "scope_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"issue_id": {
|
||||
"name": "issue_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"agent_id": {
|
||||
"name": "agent_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"responsible_user_id": {
|
||||
"name": "responsible_user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"execution_workspace_repository_access_scope_idx": {
|
||||
"name": "execution_workspace_repository_access_scope_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "company_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "root_key",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "kind",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "scope_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"execution_workspace_repository_access_company_id_companies_id_fk": {
|
||||
"name": "execution_workspace_repository_access_company_id_companies_id_fk",
|
||||
"tableFrom": "execution_workspace_repository_access",
|
||||
"tableTo": "companies",
|
||||
"columnsFrom": [
|
||||
"company_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.execution_workspace_repository_roots": {
|
||||
"name": "execution_workspace_repository_roots",
|
||||
"schema": "",
|
||||
@@ -45996,7 +46609,7 @@
|
||||
},
|
||||
"resource_lifecycle_events_action_check": {
|
||||
"name": "resource_lifecycle_events_action_check",
|
||||
"value": "\"resource_lifecycle_events\".\"action\" = 'create' OR (\"resource_lifecycle_events\".\"resource_type\" = 'project' AND \"resource_lifecycle_events\".\"action\" IN ('update', 'archive')) OR (\"resource_lifecycle_events\".\"resource_type\" = 'agent' AND \"resource_lifecycle_events\".\"action\" IN ('pause', 'resume', 'terminate'))"
|
||||
"value": "\"resource_lifecycle_events\".\"action\" = 'create' OR (\"resource_lifecycle_events\".\"resource_type\" = 'project' AND \"resource_lifecycle_events\".\"action\" IN ('update', 'archive', 'delete')) OR (\"resource_lifecycle_events\".\"resource_type\" = 'agent' AND \"resource_lifecycle_events\".\"action\" IN ('pause', 'resume', 'terminate'))"
|
||||
}
|
||||
},
|
||||
"isRLSEnabled": false
|
||||
+1234
-9
File diff suppressed because it is too large.
Load diff
+1561
-11
File diff suppressed because it is too large.
Load diff
+60657
File diff suppressed because it is too large.
Load diff
+56
@@ -2360,6 +2360,62 @@
|
||||
"when": 1791837431016,
|
||||
"tag": "0338_absurd_vengeance",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 339,
|
||||
"version": "7",
|
||||
"when": 1791903526575,
|
||||
"tag": "0339_luxuriant_iron_monger",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 340,
|
||||
"version": "7",
|
||||
"when": 1791911743153,
|
||||
"tag": "0340_open_warlock",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 341,
|
||||
"version": "7",
|
||||
"when": 1791919024567,
|
||||
"tag": "0341_abnormal_bucky",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 342,
|
||||
"version": "7",
|
||||
"when": 1791921858146,
|
||||
"tag": "0342_dry_shen",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 343,
|
||||
"version": "7",
|
||||
"when": 1791926375117,
|
||||
"tag": "0343_new_mentor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 344,
|
||||
"version": "7",
|
||||
"when": 1791930226792,
|
||||
"tag": "0344_wide_blue_marvel",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 345,
|
||||
"version": "7",
|
||||
"when": 1791978393174,
|
||||
"tag": "0345_luxuriant_carmella_unuscione",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 346,
|
||||
"version": "7",
|
||||
"when": 1791989860950,
|
||||
"tag": "0346_cloudy_whiplash",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import postgres from "postgres";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS, getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./test-embedded-postgres.js";
|
||||
|
||||
const support = await getEmbeddedPostgresTestSupport();
|
||||
const describePostgres = support.supported ? describe : describe.skip;
|
||||
|
||||
describePostgres("project skill source migration replay", () => {
|
||||
it("keeps imported content, private provenance and scheduling when renumbered SQL runs again", async () => {
|
||||
const database = await startEmbeddedPostgresTestDatabase("paperclip-project-skill-migration-");
|
||||
const sql = postgres(database.connectionString, { max: 1, onnotice: () => {} });
|
||||
try {
|
||||
const companyId = randomUUID(), projectId = randomUUID(), workspaceId = randomUUID();
|
||||
const sourceId = randomUUID(), skillId = randomUUID(), bindingId = randomUUID(), agentId = randomUUID();
|
||||
const connectionId = randomUUID();
|
||||
await sql`INSERT INTO companies (id, name, issue_prefix) VALUES (${companyId}, 'Migration company', 'PSM')`;
|
||||
await sql`INSERT INTO projects (id, company_id, name, visibility) VALUES (${projectId}, ${companyId}, 'Private project', 'private')`;
|
||||
await sql`INSERT INTO project_workspaces (id, company_id, project_id, name, repo_url) VALUES (${workspaceId}, ${companyId}, ${projectId}, 'Repository', 'https://github.com/acme/skills')`;
|
||||
await sql`INSERT INTO agents (id, company_id, name) VALUES (${agentId}, ${companyId}, 'Setup agent')`;
|
||||
await sql`INSERT INTO company_skill_sources (id, company_id, repository_url, full_name, tracking_ref, access_scope, automatic_updates, import_new_skills, refresh_user_id, refresh_agent_id, connection_id, excluded_folders, revision, failure_count, next_sync_at)
|
||||
VALUES (${sourceId}, ${companyId}, 'https://github.com/acme/skills', 'acme/skills', 'HEAD', 'projects', false, true, 'setup-person', ${agentId}, ${connectionId}, '["excluded"]', 7, 3, '2030-01-01')`;
|
||||
await sql`INSERT INTO company_skills (id, company_id, key, slug, name, markdown, access_source_id) VALUES (${skillId}, ${companyId}, 'repo/review', 'review', 'Review', 'Keep installed content', ${sourceId})`;
|
||||
await sql`INSERT INTO project_skill_sources (id, company_id, project_id, workspace_id, source_id, repository_url, tracking_ref, active, status, refresh_user_id, refresh_agent_id, connection_id)
|
||||
VALUES (${bindingId}, ${companyId}, ${projectId}, ${workspaceId}, ${sourceId}, 'https://github.com/acme/skills', 'HEAD', false, 'ready', 'setup-person', ${agentId}, ${connectionId})`;
|
||||
// Preview builds used an agent FK that would erase the authorizing identity.
|
||||
await sql.unsafe('ALTER TABLE project_skill_sources ADD CONSTRAINT project_skill_sources_refresh_agent_id_agents_id_fk FOREIGN KEY (refresh_agent_id) REFERENCES agents(id) ON DELETE SET NULL');
|
||||
const source = () => sql`SELECT * FROM company_skill_sources WHERE id = ${sourceId}`;
|
||||
const binding = () => sql`SELECT * FROM project_skill_sources WHERE id = ${bindingId}`;
|
||||
const skill = () => sql`SELECT * FROM company_skills WHERE id = ${skillId}`;
|
||||
const relations = () => sql`SELECT oid::text, relname FROM pg_class WHERE relname IN ('company_skill_sources', 'project_skill_sources', 'company_skills') ORDER BY relname`;
|
||||
const before = { source: await source(), binding: await binding(), skill: await skill(), relations: await relations() };
|
||||
const migration = await readFile(new URL("./migrations/0341_abnormal_bucky.sql", import.meta.url), "utf8");
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
for (const statement of migration.split("--> statement-breakpoint")) {
|
||||
if (statement.trim()) await sql.unsafe(statement);
|
||||
}
|
||||
expect(await source()).toEqual(before.source);
|
||||
expect(await binding()).toEqual(before.binding);
|
||||
expect(await skill()).toEqual(before.skill);
|
||||
expect(await relations()).toEqual(before.relations);
|
||||
}
|
||||
await sql`DELETE FROM agents WHERE id = ${agentId}`;
|
||||
expect((await source())[0]?.refresh_agent_id).toBe(agentId);
|
||||
expect((await binding())[0]?.refresh_agent_id).toBe(agentId);
|
||||
expect((await binding())[0]?.connection_id).toBe(connectionId);
|
||||
expect((await skill())[0]?.access_source_id).toBe(sourceId);
|
||||
} finally {
|
||||
await sql.end();
|
||||
await database.cleanup();
|
||||
}
|
||||
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
|
||||
});
|
||||
@@ -46,6 +46,7 @@ export const agents = pgTable(
|
||||
permissions: jsonb("permissions").$type<Record<string, unknown>>().notNull().default({}),
|
||||
lastHeartbeatAt: timestamp("last_heartbeat_at", { withTimezone: true }),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
||||
createdByUserId: text("created_by_user_id"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { check, pgTable, uuid, text, timestamp, integer, jsonb, index, uniqueIndex } from "drizzle-orm/pg-core";
|
||||
import { check, pgTable, uuid, text, timestamp, integer, jsonb, index, uniqueIndex, foreignKey } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { agents } from "./agents.js";
|
||||
import { companySecretProviderConfigs } from "./company_secret_provider_configs.js";
|
||||
import { userSecretDefinitions } from "./user_secret_definitions.js";
|
||||
import { plugins } from "./plugins.js";
|
||||
|
||||
export const companySecrets = pgTable(
|
||||
"company_secrets",
|
||||
@@ -12,6 +13,8 @@ export const companySecrets = pgTable(
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id),
|
||||
scope: text("scope").notNull().default("company"),
|
||||
ownerUserId: text("owner_user_id"),
|
||||
ownerAgentId: uuid("owner_agent_id"),
|
||||
ownerPluginId: uuid("owner_plugin_id").references(() => plugins.id, { onDelete: "cascade" }),
|
||||
userSecretDefinitionId: uuid("user_secret_definition_id").references(() => userSecretDefinitions.id, { onDelete: "set null" }),
|
||||
key: text("key").notNull(),
|
||||
name: text("name").notNull(),
|
||||
@@ -42,6 +45,9 @@ export const companySecrets = pgTable(
|
||||
table.ownerUserId,
|
||||
),
|
||||
companyProviderIdx: index("company_secrets_company_provider_idx").on(table.companyId, table.provider),
|
||||
agentOwner: foreignKey({ columns: [table.companyId, table.ownerAgentId], foreignColumns: [agents.companyId, agents.id] }).onDelete("cascade"),
|
||||
instanceOwnerKeyUq: uniqueIndex("company_secrets_instance_owner_key_uq").on(table.companyId, table.ownerAgentId, table.ownerPluginId, table.key)
|
||||
.where(sql`${table.scope} = 'instance' and ${table.deletedAt} is null`),
|
||||
providerConfigIdx: index("company_secrets_provider_config_idx").on(table.providerConfigId),
|
||||
companyNameUq: uniqueIndex("company_secrets_company_name_uq")
|
||||
.on(table.companyId, table.name)
|
||||
@@ -58,10 +64,16 @@ export const companySecrets = pgTable(
|
||||
${table.scope} = 'company'
|
||||
and ${table.ownerUserId} is null
|
||||
and ${table.userSecretDefinitionId} is null
|
||||
and ${table.ownerAgentId} is null and ${table.ownerPluginId} is null
|
||||
) or (
|
||||
${table.scope} = 'user'
|
||||
and ${table.ownerUserId} is not null
|
||||
and ${table.userSecretDefinitionId} is not null
|
||||
and ${table.ownerAgentId} is null and ${table.ownerPluginId} is null
|
||||
) or (
|
||||
${table.scope} = 'instance'
|
||||
and ${table.ownerUserId} is null and ${table.userSecretDefinitionId} is null
|
||||
and ${table.ownerAgentId} is not null and ${table.ownerPluginId} is not null
|
||||
)`,
|
||||
),
|
||||
}),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { pgTable, uuid, text, timestamp, jsonb, integer, boolean, uniqueIndex, index } from 'drizzle-orm/pg-core';
|
||||
import type { SkillPackageInspection, SkillRepositoryPackage } from '@paperclipai/shared';
|
||||
import { companies } from './companies.js';
|
||||
import { companySkills } from './company_skills.js';
|
||||
import { toolConnections } from './tool_access.js';
|
||||
import { projects } from './projects.js';
|
||||
import { projectWorkspaces } from './project_workspaces.js';
|
||||
|
||||
export const companySkillSources = pgTable('company_skill_sources', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
@@ -11,22 +13,65 @@ export const companySkillSources = pgTable('company_skill_sources', {
|
||||
repositoryUrl: text('repository_url').notNull(),
|
||||
fullName: text('full_name').notNull(),
|
||||
trackingRef: text('tracking_ref').notNull(),
|
||||
connectionId: uuid('connection_id').references(() => toolConnections.id, { onDelete: 'set null' }),
|
||||
// Retain the selected identity after deletion so refresh requires repair.
|
||||
connectionId: uuid('connection_id'),
|
||||
packageMode: text('package_mode').$type<'skills' | 'repository'>().notNull().default('skills'),
|
||||
repositoryPackage: jsonb('repository_package').$type<SkillRepositoryPackage>(),
|
||||
excludedFolders: jsonb('excluded_folders').$type<string[]>().notNull().default([]),
|
||||
enabled: boolean('enabled').notNull().default(true),
|
||||
// Removed project sources retain access provenance for editable copies and forks.
|
||||
removedAt: timestamp('removed_at', { withTimezone: true }),
|
||||
automaticUpdates: boolean('automatic_updates').notNull().default(true),
|
||||
importNewSkills: boolean('import_new_skills').notNull().default(false),
|
||||
accessScope: text('access_scope').$type<'company' | 'projects'>().notNull().default('company'),
|
||||
refreshUserId: text('refresh_user_id'),
|
||||
refreshAgentId: uuid('refresh_agent_id'),
|
||||
refreshLocalTrusted: boolean('refresh_local_trusted').notNull().default(false),
|
||||
refreshGrantId: uuid('refresh_grant_id'),
|
||||
nextSyncAt: timestamp('next_sync_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
failureCount: integer('failure_count').notNull().default(0),
|
||||
revision: integer('revision').notNull().default(0),
|
||||
lastAttemptAt: timestamp('last_attempt_at', { withTimezone: true }),
|
||||
lastSuccessAt: timestamp('last_success_at', { withTimezone: true }),
|
||||
lastScanCommit: text('last_scan_commit'),
|
||||
lastScanTreeSha: text('last_scan_tree_sha'),
|
||||
automaticBudgetBytes: integer('automatic_budget_bytes').notNull().default(512 * 1024 * 1024),
|
||||
automaticBudgetAt: timestamp('automatic_budget_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
lastError: text('last_error'),
|
||||
leaseToken: uuid('lease_token'),
|
||||
leaseExpiresAt: timestamp('lease_expires_at', { withTimezone: true }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
}, table => ({
|
||||
repositoryRef: uniqueIndex('company_skill_sources_repository_ref_idx').on(table.companyId, table.repositoryUrl, table.trackingRef),
|
||||
repositoryIdentity: uniqueIndex('company_skill_sources_identity_ref_idx').on(table.companyId, table.repositoryId, table.trackingRef),
|
||||
repositoryRef: uniqueIndex('company_skill_sources_repository_ref_idx').on(table.companyId, table.repositoryUrl, table.trackingRef).where(sql`${table.removedAt} is null`),
|
||||
repositoryIdentity: uniqueIndex('company_skill_sources_identity_ref_idx').on(table.companyId, table.repositoryId, table.trackingRef).where(sql`${table.removedAt} is null`),
|
||||
}));
|
||||
|
||||
/** Durable attachment jobs and retained project access provenance. */
|
||||
export const projectSkillSources = pgTable('project_skill_sources', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id').notNull().references(() => companies.id, { onDelete: 'cascade' }),
|
||||
projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
|
||||
workspaceId: uuid('workspace_id').references(() => projectWorkspaces.id, { onDelete: 'set null' }),
|
||||
sourceId: uuid('source_id').references(() => companySkillSources.id, { onDelete: 'set null' }),
|
||||
repositoryUrl: text('repository_url').notNull(),
|
||||
trackingRef: text('tracking_ref').notNull(),
|
||||
active: boolean('active').notNull().default(true),
|
||||
status: text('status').$type<'pending' | 'ready' | 'error'>().notNull().default('pending'),
|
||||
refreshUserId: text('refresh_user_id'),
|
||||
refreshAgentId: uuid('refresh_agent_id'),
|
||||
refreshLocalTrusted: boolean('refresh_local_trusted').notNull().default(false),
|
||||
connectionId: uuid('connection_id'),
|
||||
refreshGrantId: uuid('refresh_grant_id'),
|
||||
nextSyncAt: timestamp('next_sync_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
failureCount: integer('failure_count').notNull().default(0),
|
||||
lastError: text('last_error'),
|
||||
leaseToken: uuid('lease_token'),
|
||||
leaseExpiresAt: timestamp('lease_expires_at', { withTimezone: true }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
}, table => ({
|
||||
workspaceRepository: uniqueIndex('project_skill_sources_workspace_repo_ref_idx').on(table.workspaceId, table.repositoryUrl, table.trackingRef),
|
||||
sourceProject: index('project_skill_sources_source_project_idx').on(table.companyId, table.sourceId, table.projectId),
|
||||
pending: index('project_skill_sources_sync_idx').on(table.active, table.status, table.nextSyncAt),
|
||||
}));
|
||||
export const companySkillSourceEntries = pgTable('company_skill_source_entries', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
@@ -44,3 +89,10 @@ export const companySkillSourceEntries = pgTable('company_skill_source_entries',
|
||||
sourcePath: uniqueIndex('company_skill_source_entries_source_path_idx').on(table.sourceId, table.path),
|
||||
companySkill: index('company_skill_source_entries_company_skill_idx').on(table.companyId, table.skillId),
|
||||
}));
|
||||
|
||||
/** Sustained company credits for automatic initial imports, including failures. */
|
||||
export const companySkillImportBudgets = pgTable('company_skill_import_budgets', {
|
||||
companyId: uuid('company_id').primaryKey().references(() => companies.id, { onDelete: 'cascade' }),
|
||||
availableBytes: integer('available_bytes').notNull().default(512 * 1024 * 1024),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
@@ -40,6 +40,8 @@ export const companySkills = pgTable(
|
||||
homepageUrl: text("homepage_url"),
|
||||
categories: text("categories").array().notNull().default([]),
|
||||
sharingScope: text("sharing_scope").$type<CompanySkillSharingScope>().notNull().default("company"),
|
||||
// Source access is inherited by editable copies too. Orphaned sources fail closed.
|
||||
accessSourceId: uuid("access_source_id"),
|
||||
publicShareToken: text("public_share_token"),
|
||||
forkedFromSkillId: uuid("forked_from_skill_id").references((): AnyPgColumn => companySkills.id, { onDelete: "set null" }),
|
||||
forkedFromCompanyId: uuid("forked_from_company_id").references(() => companies.id, { onDelete: "set null" }),
|
||||
|
||||
@@ -11,6 +11,7 @@ export const dotAgentBindings = pgTable("dot_agent_bindings", {
|
||||
agentId: uuid("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }),
|
||||
operatorId: text("operator_id").notNull(),
|
||||
grantId: uuid("grant_id").references(() => mcpOauthGrants.id, { onDelete: "set null" }),
|
||||
providerKind: text("provider_kind").$type<"openai_dot" | "grok_bot">().notNull().default("openai_dot"),
|
||||
generation: integer("generation").notNull().default(1),
|
||||
status: text("status").$type<"pairing" | "connected" | "ready" | "revoked">().notNull().default("pairing"),
|
||||
pairingCodeHash: text("pairing_code_hash"),
|
||||
@@ -63,6 +64,8 @@ export const dotRunnerOperations = pgTable("dot_runner_operations", {
|
||||
command: jsonb("command").$type<Record<string, unknown>>().notNull(),
|
||||
status: text("status").$type<"reserved" | "admitted" | "completed" | "rejected" | "unknown">().notNull().default("reserved"),
|
||||
outcome: jsonb("outcome").$type<Record<string, unknown>>(),
|
||||
pendingReturnedAt: timestamp("pending_returned_at", { withTimezone: true }),
|
||||
observedAt: timestamp("observed_at", { withTimezone: true }),
|
||||
sourceEventId: text("source_event_id"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { index, integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { agents } from "./agents.js";
|
||||
import { companySecrets } from "./company_secrets.js";
|
||||
import { dotAgentBindings, dotRunnerAssignments } from "./dot_runner.js";
|
||||
import { mcpOauthGrants } from "./public_mcp.js";
|
||||
|
||||
// Keep the shared binding/assignment tables and their unique ownership constraints.
|
||||
// Routine material is one encrypted secret version, never an agent config field.
|
||||
export const grokBotRoutines = pgTable("grok_bot_routines", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
agentId: uuid("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }),
|
||||
bindingId: uuid("binding_id").notNull().references(() => dotAgentBindings.id, { onDelete: "cascade" }),
|
||||
bindingGeneration: integer("binding_generation").notNull(),
|
||||
botName: text("bot_name").notNull(),
|
||||
secretId: uuid("secret_id").notNull().references(() => companySecrets.id),
|
||||
secretVersion: integer("secret_version").notNull(),
|
||||
revision: integer("revision").notNull().default(1),
|
||||
verifiedGrantId: uuid("verified_grant_id").references(() => mcpOauthGrants.id, { onDelete: "set null" }),
|
||||
verifiedAt: timestamp("verified_at", { withTimezone: true }),
|
||||
lastContactAt: timestamp("last_contact_at", { withTimezone: true }),
|
||||
recoveryRequiredAt: timestamp("recovery_required_at", { withTimezone: true }),
|
||||
recoveryReason: text("recovery_reason").$type<"dispatch" | "work_session">(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, t => [uniqueIndex("grok_routines_binding_uq").on(t.bindingId), index("grok_routines_company_agent_idx").on(t.companyId, t.agentId)]);
|
||||
|
||||
export const grokBotDispatches = pgTable("grok_bot_dispatches", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
routineId: uuid("routine_id").notNull().references(() => grokBotRoutines.id, { onDelete: "cascade" }),
|
||||
bindingId: uuid("binding_id").notNull().references(() => dotAgentBindings.id, { onDelete: "cascade" }),
|
||||
bindingGeneration: integer("binding_generation").notNull(),
|
||||
grantId: uuid("grant_id").notNull().references(() => mcpOauthGrants.id, { onDelete: "cascade" }),
|
||||
routineRevision: integer("routine_revision").notNull(),
|
||||
assignmentId: uuid("assignment_id").references(() => dotRunnerAssignments.id, { onDelete: "cascade" }),
|
||||
sourceEventId: text("source_event_id").notNull(),
|
||||
kind: text("kind").$type<"verify" | "mailbox_updated" | "authority_revoked">().notNull(),
|
||||
// Closed references only. Challenge possession alone carries no authority.
|
||||
payload: jsonb("payload").$type<Record<string, unknown>>().notNull(),
|
||||
challengeHash: text("challenge_hash"),
|
||||
status: text("status").$type<"prepared" | "sending" | "accepted" | "rejected" | "unknown" | "acknowledged" | "cancelled">().notNull().default("prepared"),
|
||||
httpStatus: integer("http_status"),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
sentAt: timestamp("sent_at", { withTimezone: true }),
|
||||
acknowledgedAt: timestamp("acknowledged_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, t => [
|
||||
uniqueIndex("grok_dispatch_event_uq").on(t.bindingId, t.bindingGeneration, t.routineRevision, t.sourceEventId),
|
||||
index("grok_dispatch_company_status_idx").on(t.companyId, t.status),
|
||||
index("grok_dispatch_assignment_kind_idx").on(t.assignmentId, t.kind),
|
||||
]);
|
||||
@@ -189,5 +189,17 @@ export const heartbeatRuns = pgTable(
|
||||
sql`(${table.contextSnapshot} ->> 'taskKey')`,
|
||||
table.createdAt.desc(),
|
||||
),
|
||||
companyCtxPaperclipIssueCreatedIdx: index("heartbeat_runs_company_ctx_paperclip_issue_created_idx")
|
||||
.on(table.companyId, sql`(${table.contextSnapshot} -> 'paperclipIssue' ->> 'id')`, table.createdAt.desc())
|
||||
.concurrently(),
|
||||
// Match creation provenance precedence, including empty legacy IDs.
|
||||
companyOriginIssueIdx: index("heartbeat_runs_company_origin_issue_idx")
|
||||
.on(table.companyId, sql`coalesce(
|
||||
${table.nativeIssueId}::text,
|
||||
nullif(${table.contextSnapshot}->>'issueId', ''),
|
||||
nullif(${table.contextSnapshot}->>'taskId', ''),
|
||||
nullif(${table.contextSnapshot}->>'taskKey', '')
|
||||
)`)
|
||||
.concurrently(),
|
||||
}),
|
||||
);
|
||||
@@ -227,6 +227,7 @@ export { browserUseSettings, browserUseSessions, browserUseRuns, browserUseBrows
|
||||
export * from "./company_skill_sources.js";
|
||||
export * from "./public_mcp.js";
|
||||
export * from "./dot_runner.js";
|
||||
export * from "./grok_bot.js";
|
||||
|
||||
|
||||
export * from "./accounting.js";
|
||||
|
||||
@@ -170,6 +170,8 @@ export const mcpEventDeliveries = pgTable("mcp_event_deliveries", {
|
||||
|
||||
// Upload identities outlive their short-lived tickets so retries cannot duplicate files.
|
||||
export const mcpAttachmentUploads = pgTable("mcp_attachment_uploads", {
|
||||
grantId: uuid("grant_id").references(() => mcpOauthGrants.id, { onDelete: "cascade" }),
|
||||
workSessionId: uuid("work_session_id"),
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id").notNull().references(() => authUsers.id, { onDelete: "cascade" }),
|
||||
@@ -193,6 +195,7 @@ export const mcpAttachmentUploads = pgTable("mcp_attachment_uploads", {
|
||||
]);
|
||||
|
||||
export const mcpFileTickets = pgTable("mcp_file_tickets", {
|
||||
workSessionId: uuid("work_session_id"),
|
||||
tokenHash: text("token_hash").primaryKey(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
grantId: uuid("grant_id").notNull().references(() => mcpOauthGrants.id, { onDelete: "cascade" }),
|
||||
|
||||
@@ -8,12 +8,12 @@ export const resourceLifecycleEvents = pgTable("resource_lifecycle_events", {
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
resourceType: text("resource_type").$type<"agent" | "project">().notNull(),
|
||||
resourceId: uuid("resource_id").notNull(),
|
||||
action: text("action").$type<"create" | "update" | "archive" | "pause" | "resume" | "terminate">().notNull(),
|
||||
action: text("action").$type<"create" | "update" | "archive" | "delete" | "pause" | "resume" | "terminate">().notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
}, (table) => ({
|
||||
resourceIdx: uniqueIndex("resource_lifecycle_events_creation_idx").on(table.companyId, table.resourceType, table.resourceId).where(sql`${table.action} = 'create'`),
|
||||
companyIdx: index("resource_lifecycle_events_company_idx").on(table.companyId, table.id),
|
||||
resourceOrderIdx: index("resource_lifecycle_events_resource_order_idx").on(table.companyId, table.resourceType, table.resourceId, table.id),
|
||||
resourceTypeCheck: check("resource_lifecycle_events_resource_type_check", sql`${table.resourceType} IN ('agent', 'project')`),
|
||||
actionCheck: check("resource_lifecycle_events_action_check", sql`${table.action} = 'create' OR (${table.resourceType} = 'project' AND ${table.action} IN ('update', 'archive')) OR (${table.resourceType} = 'agent' AND ${table.action} IN ('pause', 'resume', 'terminate'))`),
|
||||
actionCheck: check("resource_lifecycle_events_action_check", sql`${table.action} = 'create' OR (${table.resourceType} = 'project' AND ${table.action} IN ('update', 'archive', 'delete')) OR (${table.resourceType} = 'agent' AND ${table.action} IN ('pause', 'resume', 'terminate'))`),
|
||||
}));
|
||||
@@ -48,6 +48,24 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
expect(await db.execute(sql`select * from work_signal_test where id = 'rollback'`)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("coalesces scoped hints and publishes them only when the outer transaction settles", async () => {
|
||||
const settled: string[][] = [];
|
||||
const unsubscribe = subscribeDatabaseWork(db, "scoped", (event, hints) => {
|
||||
if (event === "settled") settled.push([...(hints ?? [])]);
|
||||
});
|
||||
try {
|
||||
await db.transaction(async tx => {
|
||||
await signalDatabaseWork(tx, "scoped", "connection-a");
|
||||
await tx.transaction(async nested => {
|
||||
await signalDatabaseWork(nested, "scoped", "connection-b");
|
||||
await signalDatabaseWork(nested, "scoped", "connection-a");
|
||||
});
|
||||
expect(settled).toEqual([]);
|
||||
});
|
||||
expect(settled).toEqual([["connection-a", "connection-b"]]);
|
||||
} finally { unsubscribe(); }
|
||||
});
|
||||
|
||||
it("keeps outer ownership after a caught savepoint rollback", async () => {
|
||||
const events: string[] = [];
|
||||
const unsubscribe = subscribeDatabaseWork(db, "savepoint", event => events.push(event));
|
||||
@@ -92,19 +110,25 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
}) as typeof raw.transaction;
|
||||
const owner = installDatabaseWorkSignals(raw);
|
||||
const topic = `late-${outcome}`;
|
||||
const settled: string[][] = [];
|
||||
const unsubscribe = subscribeDatabaseWork(owner, topic, (event, hints) => {
|
||||
if (event === "settled") settled.push([...(hints ?? [])]);
|
||||
});
|
||||
try {
|
||||
await expect(owner.transaction(async tx => {
|
||||
await signalDatabaseWork(tx, topic);
|
||||
await signalDatabaseWork(tx, topic, "connection-late");
|
||||
await tx.execute(sql`insert into work_signal_test values (${topic})`);
|
||||
})).rejects.toThrow("lost transaction response");
|
||||
await reconcileDatabaseWork(owner, topic);
|
||||
expect(databaseWorkPending(owner, topic)).toBe(true);
|
||||
expect(await db.execute(sql`select * from work_signal_test where id = ${topic}`)).toHaveLength(0);
|
||||
settled.length = 0;
|
||||
release(); await serverDone.catch(() => {});
|
||||
await reconcileDatabaseWork(owner, topic);
|
||||
expect(settled).toEqual([["connection-late"]]);
|
||||
expect(databaseWorkPending(owner, topic)).toBe(false);
|
||||
expect(await db.execute(sql`select * from work_signal_test where id = ${topic}`)).toHaveLength(outcome === "commit" ? 1 : 0);
|
||||
} finally { release(); await serverDone?.catch(() => {}); }
|
||||
} finally { release(); await serverDone?.catch(() => {}); unsubscribe(); }
|
||||
});
|
||||
|
||||
it("recovers after a real backend disconnect without replaying the queue write", async () => {
|
||||
|
||||
@@ -3,16 +3,16 @@ import { sql } from "drizzle-orm";
|
||||
// Process-local hints about existing durable queues. No SQL triggers, tables,
|
||||
// background timers, or driver query replay. Only opted-in transactions fetch
|
||||
// an XID, so unrelated transactions keep their existing SQL behavior.
|
||||
type Listener = (event: "intent" | "settled") => void;
|
||||
type Transaction = { topics: Set<string>; xid?: Promise<string>; uncertain: boolean };
|
||||
type Listener = (event: "intent" | "settled", hints?: ReadonlySet<string>) => void;
|
||||
type Transaction = { topics: Set<string>; hints: Map<string, Set<string>>; xid?: Promise<string>; uncertain: boolean };
|
||||
type Scope = { listeners: Map<string, Set<Listener>>; transactions: Set<Transaction>; root: Connection };
|
||||
type Context = { scope: Scope; transaction?: Transaction };
|
||||
type Connection = { transaction: (...args: any[]) => Promise<any>; execute: (...args: any[]) => Promise<any> };
|
||||
const contexts = new WeakMap<object, Context>();
|
||||
|
||||
function publish(scope: Scope, topic: string, event: "intent" | "settled" = "settled") {
|
||||
function publish(scope: Scope, topic: string, event: "intent" | "settled" = "settled", hints?: ReadonlySet<string>) {
|
||||
for (const listener of scope.listeners.get(topic) ?? []) {
|
||||
try { listener(event); }
|
||||
try { listener(event, hints); }
|
||||
catch (error) { process.emitWarning(`Database work listener failed: ${String(error)}`); }
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ function install(connection: Connection, context: Context) {
|
||||
connection.transaction = async function (callback, ...options) {
|
||||
// Nested savepoints belong to the outermost transaction. Even a released
|
||||
// savepoint must not wake a consumer before its parent commits.
|
||||
const state = context.transaction ?? { topics: new Set<string>(), uncertain: false };
|
||||
const state = context.transaction ?? { topics: new Set<string>(), hints: new Map<string, Set<string>>(), uncertain: false };
|
||||
try {
|
||||
const result = await transaction.call(this, async (tx: Connection) => {
|
||||
install(tx, { scope: context.scope, transaction: state });
|
||||
@@ -47,20 +47,25 @@ function install(connection: Connection, context: Context) {
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (!context.transaction) for (const topic of state.topics) publish(context.scope, topic);
|
||||
if (!context.transaction) for (const topic of state.topics) publish(context.scope, topic, "settled", state.hints.get(topic));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Await BEFORE the queue write, on its transaction. Nested transactions work. */
|
||||
export async function signalDatabaseWork(connection: object, topic: string): Promise<void> {
|
||||
export async function signalDatabaseWork(connection: object, topic: string, hint?: string): Promise<void> {
|
||||
const context = contexts.get(connection);
|
||||
if (!context) return; // Lightweight service test doubles have no DB lifecycle.
|
||||
const state = context.transaction;
|
||||
if (!state) throw new Error("Database work must be registered inside a transaction");
|
||||
state.topics.add(topic);
|
||||
if (hint !== undefined) {
|
||||
let hints = state.hints.get(topic);
|
||||
if (!hints) state.hints.set(topic, hints = new Set());
|
||||
hints.add(hint);
|
||||
}
|
||||
context.scope.transactions.add(state);
|
||||
publish(context.scope, topic, "intent");
|
||||
publish(context.scope, topic, "intent", state.hints.get(topic));
|
||||
if (!state.xid) {
|
||||
state.xid = (connection as Connection).execute(sql`select pg_current_xact_id()::text as xid`)
|
||||
.then((rows: { xid: string }[]) => rows[0]!.xid);
|
||||
@@ -91,7 +96,7 @@ export async function reconcileDatabaseWork(owner: object, topic: string): Promi
|
||||
throw new Error("Unexpected database transaction status");
|
||||
}
|
||||
scope.transactions.delete(state);
|
||||
for (const affected of state.topics) publish(scope, affected);
|
||||
for (const affected of state.topics) publish(scope, affected, "settled", state.hints.get(affected));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"copilot": {
|
||||
"agentProfileVersion": 18,
|
||||
"agentProfileVersion": 19,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
"commandDigest": "sha256:a4c7e955df18bf2448224ecce2120b43ede8054fe00e7b33b4044b8bd284b321",
|
||||
"requiresProviderPolicy": true,
|
||||
"qualificationStatus": "pending"
|
||||
},
|
||||
|
||||
@@ -304,3 +304,15 @@ not part of the contract. Native attempt output is
|
||||
fields but rejects unknown required versions and inconsistent terminal,
|
||||
semantic-denial, usage, or transcript facts. Full fields and the deterministic
|
||||
gate are exercised by the package-local deterministic conformance suite.
|
||||
|
||||
|
||||
## Claude effort after the Codex default merge
|
||||
|
||||
Native input v7 adds configured Claude effort while retaining v1–v6 readers,
|
||||
including v6 Dot and Grok Bot bindings. The shared executor retains the merged
|
||||
Codex close/cleanup implementation. Copilot's shared source attestation advances
|
||||
to pending profile 19 rather than overwriting the merged profile-18 fixture;
|
||||
its provider executable and permission closure are unchanged. Historical private
|
||||
Claude profile-18 evidence remains source-bound under its separate fixture name.
|
||||
Pi profile 25 retains its existing identity. These source identities require
|
||||
fresh packaged compatibility checks; earlier provider runs are not relabeled.
|
||||
@@ -1 +1 @@
|
||||
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaimLine truncated
|
||||
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaimLine truncated
|
||||
@@ -112,7 +112,7 @@
|
||||
"planning",
|
||||
"skill_test"
|
||||
],
|
||||
"description": "Fallback only: call a discovered Paperclip API operation when dedicated tools lack the required operation or parameters. Uses your existing permissions. Prefer dedicated tools; never bypass a denial or runner lifecycle tool. For large text responses, read the returned artifact with GET /api/assets/{assetId}/content and responseText; follow nextOffsetBytes until null.",
|
||||
"description": "Fallback only: call a discovered Paperclip API operation when dedicated tools lack the required operation or parameters. Uses the same route permissions as agent REST access. Prefer dedicated tools for common workflows; route denials still apply. For large text responses, read the returned artifact with GET /api/assets/{assetId}/content and responseText; follow nextOffsetBytes until null.",
|
||||
"effect": "write",
|
||||
"inputSchema": {
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"prpVersion": 1,
|
||||
"nativeExecutionVersion": 1,
|
||||
"catalogVersion": 1,
|
||||
"catalogSha256": "sha256:73f19996d9e1b46e957ba732354eb24daf4e96776ae121ea059acfaf01f56ef6",
|
||||
"catalogSha256": "sha256:2de31fff50aed0a7b28e222084695cae5d7ba04612737881114edff0dbc2781c",
|
||||
"driverContractVersion": 1,
|
||||
"driverKind": "paperclip-deterministic",
|
||||
"driverVersion": "1.0.0"
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
{
|
||||
"path": "schemas/provider-descriptor.schema.json",
|
||||
"id": "https://paperclip.dev/schemas/prp/v1/provider-descriptor.schema.json",
|
||||
"sha256": "ef37ebba071ac239e0b4ff680a469eca6948b8eace451c5c88089c81b7acdae1"
|
||||
"sha256": "efe692b932e6db17006349a089d55e2ba9d5f8e468caffacdefa1c5dcd7d958c"
|
||||
},
|
||||
{
|
||||
"path": "schemas/provider-event.schema.json",
|
||||
@@ -175,7 +175,7 @@
|
||||
},
|
||||
{
|
||||
"path": "fixtures/evals/native-execution-seeded.json",
|
||||
"sha256": "872f556fd39a4b7e50a902d09c8b42f8fc729030d03642983fa16872f623e1ef",
|
||||
"sha256": "a564abe2d4dd502913250c93a0bbdd6dc06f2556efff6d93d577fcb9011e4a70",
|
||||
"expectation": "accept",
|
||||
"compatibilityCase": "canonical"
|
||||
},
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"claude_managed",
|
||||
"aws_agentcore",
|
||||
"acpx",
|
||||
"openai_dot"
|
||||
"openai_dot",
|
||||
"grok_bot"
|
||||
]
|
||||
},
|
||||
"driver": {
|
||||
@@ -28,7 +29,8 @@
|
||||
"claude_managed_agents_api",
|
||||
"aws_agentcore_harness_api",
|
||||
"acpx_runtime",
|
||||
"openai_dot_mcp"
|
||||
"openai_dot_mcp",
|
||||
"grok_bot_mcp"
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
@@ -53,7 +55,8 @@
|
||||
"enum": [
|
||||
"anthropic_managed_agents",
|
||||
"aws_bedrock_agentcore_harness",
|
||||
"openai_dot"
|
||||
"openai_dot",
|
||||
"grok_bot"
|
||||
]
|
||||
},
|
||||
"providerSessionId": {
|
||||
@@ -284,6 +287,19 @@
|
||||
"const": "remote_service"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "grok_bot"
|
||||
},
|
||||
"driver": {
|
||||
"const": "grok_bot_mcp"
|
||||
},
|
||||
"executionKind": {
|
||||
"const": "remote_service"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -422,6 +438,38 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "grok_bot"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"service",
|
||||
"providerSessionId"
|
||||
],
|
||||
"properties": {
|
||||
"service": {
|
||||
"const": "grok_bot"
|
||||
},
|
||||
"model": {
|
||||
"const": null
|
||||
},
|
||||
"providerSessionId": {
|
||||
"const": null
|
||||
},
|
||||
"agentProcessId": {
|
||||
"const": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"$defs": {
|
||||
|
||||
@@ -120,7 +120,7 @@ fn build_metadata() -> serde_json::Value {
|
||||
"packageVersion": env!("CARGO_PKG_VERSION"),
|
||||
"binaryContractVersion": 2,
|
||||
"durableSessionCapabilities": ["unlimited_runtime", "connection_lease_renewal"],
|
||||
"externalProviderCapabilities": ["openai_dot_mcp"],
|
||||
"externalProviderCapabilities": ["openai_dot_mcp", "grok_bot_mcp"],
|
||||
"nativeExecutionVersion": 1,
|
||||
"harnessDriverVersion": 1,
|
||||
"prp": {
|
||||
@@ -448,7 +448,7 @@ mod tests {
|
||||
assert_eq!(metadata["binaryContractVersion"], 2);
|
||||
assert_eq!(
|
||||
metadata["externalProviderCapabilities"],
|
||||
json!(["openai_dot_mcp"])
|
||||
json!(["openai_dot_mcp", "grok_bot_mcp"])
|
||||
);
|
||||
assert_eq!(
|
||||
metadata["durableSessionCapabilities"],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Dot is an external service. This executor owns the durable turn and tool
|
||||
//! Dot and Grok Bot are external services. This executor owns the durable turn and tool
|
||||
//! receipts; the authenticated controller owns OAuth, the mailbox and policy.
|
||||
//! No network credential or callback address enters this provider descriptor.
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
@@ -99,6 +99,22 @@ fn bounded_id(value: &str) -> bool {
|
||||
.all(|c| c.is_ascii_alphanumeric() || b"._:-".contains(&c))
|
||||
}
|
||||
|
||||
fn external_driver(kind: &str) -> &'static str {
|
||||
if kind == "grok_bot" {
|
||||
"grok_bot_mcp"
|
||||
} else {
|
||||
"openai_dot_mcp"
|
||||
}
|
||||
}
|
||||
|
||||
fn external_revision(kind: &str) -> &'static str {
|
||||
if kind == "grok_bot" {
|
||||
"grok-bot-mcp-v1"
|
||||
} else {
|
||||
"dot-mcp-v1"
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn validate(&self, config: &DurableRunnerConfig) -> Result<(), DurableRunnerError> {
|
||||
let d = &self.descriptor;
|
||||
@@ -106,7 +122,7 @@ impl State {
|
||||
|| self.run_id != config.run_id
|
||||
|| self.session_id != config.normalized_session_id
|
||||
|| self.turn_id != config.turn_id
|
||||
|| d.kind != "openai_dot"
|
||||
|| !matches!(d.kind.as_str(), "openai_dot" | "grok_bot")
|
||||
|| !bounded_id(&d.company_id)
|
||||
|| !bounded_id(&d.agent_id)
|
||||
|| !bounded_id(&d.binding_id)
|
||||
@@ -343,7 +359,13 @@ impl DotCommandExecutor {
|
||||
next_event: 1,
|
||||
});
|
||||
}
|
||||
Ok(json!({"status":"prepared","provider":"openai_dot","driver":"openai_dot_mcp"}))
|
||||
let s = self
|
||||
.state
|
||||
.as_ref()
|
||||
.ok_or_else(|| invalid("External bot is not prepared"))?;
|
||||
Ok(
|
||||
json!({"status":"prepared","provider":s.descriptor.kind,"driver":external_driver(&s.descriptor.kind)}),
|
||||
)
|
||||
}
|
||||
|
||||
fn operation(&mut self, value: &Value) -> Result<Value, DurableRunnerError> {
|
||||
@@ -384,7 +406,7 @@ impl DotCommandExecutor {
|
||||
return Err(invalid("Dot assignment is unavailable for acceptance"));
|
||||
}
|
||||
state.lifecycle = "running".to_owned();
|
||||
state.push("turn.started", json!({"provider":"openai_dot","providerTurnId":state.turn_id,"turnId":state.turn_id}))?;
|
||||
state.push("turn.started", json!({"provider":state.descriptor.kind,"providerTurnId":state.turn_id,"turnId":state.turn_id}))?;
|
||||
json!({"status":"accepted"})
|
||||
}
|
||||
"tool" => {
|
||||
@@ -432,7 +454,7 @@ impl DotCommandExecutor {
|
||||
.as_str()
|
||||
.filter(|s| !s.is_empty() && s.len() <= 12000)
|
||||
.ok_or_else(|| invalid("Dot progress must be bounded text"))?;
|
||||
state.push("item.completed", json!({"provider":"openai_dot","item":{"id":id,"type":"agentMessage","text":text,"phase":"commentary"}}))?;
|
||||
state.push("item.completed", json!({"provider":state.descriptor.kind,"item":{"id":id,"type":"agentMessage","text":text,"phase":"commentary"}}))?;
|
||||
json!({"status":"completed"})
|
||||
}
|
||||
"finish" => {
|
||||
@@ -455,8 +477,8 @@ impl DotCommandExecutor {
|
||||
.settle_turn("completed")
|
||||
.map_err(|e| invalid(e.to_string()))?;
|
||||
state.push("run.result.proposed", result.clone())?;
|
||||
state.push("item.completed", json!({"provider":"openai_dot","item":{"id":id,"type":"agentMessage","text":result["summary"],"phase":"final_answer"}}))?;
|
||||
state.push("turn.completed", json!({"provider":"openai_dot","providerTurnId":state.turn_id,"status":"completed"}))?;
|
||||
state.push("item.completed", json!({"provider":state.descriptor.kind,"item":{"id":id,"type":"agentMessage","text":result["summary"],"phase":"final_answer"}}))?;
|
||||
state.push("turn.completed", json!({"provider":state.descriptor.kind,"providerTurnId":state.turn_id,"status":"completed"}))?;
|
||||
state.push("run.terminal", json!({"schema":"paperclip.prp.terminal.v1","turnTerminalState":"completed","runTerminalState":"succeeded","reportedWorkDisposition":result["reportedWorkDisposition"]}))?;
|
||||
json!({"status":"completed"})
|
||||
}
|
||||
@@ -566,9 +588,9 @@ impl DotCommandExecutor {
|
||||
.ok_or_else(|| invalid("Dot is not prepared"))?;
|
||||
if s.lifecycle == "prepared" {
|
||||
s.lifecycle = "open".to_owned();
|
||||
s.push("session.started", json!({"provider":"openai_dot","driver":"openai_dot_mcp","providerVersion":"dot-mcp-v1",
|
||||
s.push("session.started", json!({"provider":s.descriptor.kind,"driver":external_driver(&s.descriptor.kind),"providerVersion":external_revision(&s.descriptor.kind),
|
||||
"driverSessionId":s.session_id,"providerSessionId":null,"sessionId":s.session_id,"model":null,
|
||||
"providerDescriptor":{"provider":"openai_dot","driver":"openai_dot_mcp","executionKind":"remote_service","service":"openai_dot","model":null,"providerVersion":"unknown","providerSessionId":null,"processId":null}}))?;
|
||||
"providerDescriptor":{"provider":s.descriptor.kind,"driver":external_driver(&s.descriptor.kind),"executionKind":"remote_service","service":s.descriptor.kind,"model":null,"providerVersion":"unknown","providerSessionId":null,"processId":null}}))?;
|
||||
}
|
||||
Ok(
|
||||
json!({"status":"started","driverSessionId":s.session_id,"providerSessionId":null}),
|
||||
@@ -604,7 +626,7 @@ impl DotCommandExecutor {
|
||||
.as_ref()
|
||||
.ok_or_else(|| invalid("Dot is not prepared"))?;
|
||||
Ok(
|
||||
json!({"status":s.lifecycle,"provider":"openai_dot","driver":"openai_dot_mcp","driverSessionId":s.session_id,
|
||||
json!({"status":s.lifecycle,"provider":s.descriptor.kind,"driver":external_driver(&s.descriptor.kind),"driverSessionId":s.session_id,
|
||||
"providerSessionId":null,"activeProviderTurnId":if matches!(s.lifecycle.as_str(), "offered" | "running") {Some(&s.turn_id)} else {None},
|
||||
"operationCount":s.operations.len(),"pendingTools":s.tools.pending_calls().count(),"binding":s.binding()}),
|
||||
)
|
||||
@@ -620,7 +642,7 @@ impl DotCommandExecutor {
|
||||
.settle_turn("authority_revoked")
|
||||
.map_err(|e| invalid(e.to_string()))?;
|
||||
s.push("external_provider.dispatch_requested", json!({"binding":s.binding(),"kind":"authority_revoked","externalStopConfirmed":false}))?;
|
||||
s.push("turn.cancelled", json!({"provider":"openai_dot","providerTurnId":s.turn_id,"externalStopConfirmed":false}))?;
|
||||
s.push("turn.cancelled", json!({"provider":s.descriptor.kind,"providerTurnId":s.turn_id,"externalStopConfirmed":false}))?;
|
||||
s.push("run.terminal", json!({"schema":"paperclip.prp.terminal.v1","turnTerminalState":"cancelled","runTerminalState":"cancelled","reportedWorkDisposition":"needs_review"}))?;
|
||||
}
|
||||
Ok(
|
||||
@@ -680,7 +702,7 @@ impl CommandExecutor for DotCommandExecutor {
|
||||
let expiry = (|| -> Result<(), DurableRunnerError> {
|
||||
s.tools.settle_turn("authority_expired").map_err(|e| invalid(e.to_string()))?;
|
||||
s.push("external_provider.dispatch_requested", json!({"binding":s.binding(),"kind":"authority_revoked","reason":"authority_expired","externalStopConfirmed":false}))?;
|
||||
s.push("turn.failed", json!({"provider":"openai_dot","providerTurnId":s.turn_id,"error":{"code":"dot_assignment_expired","message":"Dot assignment acceptance or execution authority expired."}}))?;
|
||||
s.push("turn.failed", json!({"provider":s.descriptor.kind,"providerTurnId":s.turn_id,"error":{"code":"dot_assignment_expired","message":"Dot assignment acceptance or execution authority expired."}}))?;
|
||||
s.push("run.terminal", json!({"schema":"paperclip.prp.terminal.v1","turnTerminalState":"failed","runTerminalState":"failed","reportedWorkDisposition":"needs_review"}))?;
|
||||
Ok(())
|
||||
})().and_then(|_| self.save());
|
||||
@@ -791,6 +813,10 @@ mod tests {
|
||||
"digest":semantic_value_digest(&json!({"action":action,"input":input})),"action":action,"input":input})
|
||||
}
|
||||
fn prepared(path: &std::path::Path) -> DotCommandExecutor {
|
||||
prepared_for(path, "openai_dot")
|
||||
}
|
||||
|
||||
fn prepared_for(path: &std::path::Path, provider: &str) -> DotCommandExecutor {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
@@ -817,7 +843,7 @@ mod tests {
|
||||
call(
|
||||
&mut executor,
|
||||
"run.prepare",
|
||||
json!({"provider":{"kind":"openai_dot","companyId":"company-1","agentId":"agent-1",
|
||||
json!({"provider":{"kind":provider,"companyId":"company-1","agentId":"agent-1",
|
||||
"bindingId":"binding-1","bindingGeneration":1,"acceptByUnixMs":now+600000,"expiresAtUnixMs":now+7200000,"instructions":"test"},
|
||||
"completionContract":{"revision":"contract-1","criterionIds":["criterion-1"]},"authorizedTools":catalog}),
|
||||
);
|
||||
@@ -832,6 +858,61 @@ mod tests {
|
||||
operation("accept-1", "accept", json!({})),
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn grok_identity_survives_recovery_and_revocation_without_claiming_remote_termination() {
|
||||
let dir = TestDirectory::new();
|
||||
let mut executor = prepared_for(dir.path(), "grok_bot");
|
||||
let events = executor.poll_events().unwrap();
|
||||
assert!(events.iter().any(|e| e.event_type == "session.started"
|
||||
&& e.payload["provider"] == "grok_bot"
|
||||
&& e.payload["driver"] == "grok_bot_mcp"));
|
||||
accept(&mut executor);
|
||||
let progress = operation(
|
||||
"progress-1",
|
||||
"progress",
|
||||
json!({"text":"Synthetic Grok progress"}),
|
||||
);
|
||||
let result = call(
|
||||
&mut executor,
|
||||
"external_provider.operation",
|
||||
progress.clone(),
|
||||
);
|
||||
drop(executor);
|
||||
let mut recovered = DotCommandExecutor::with_runner_config(dir.path(), &config(dir.path()));
|
||||
assert_eq!(
|
||||
call(&mut recovered, "external_provider.operation", progress),
|
||||
result
|
||||
);
|
||||
let snapshot = call(&mut recovered, "session.snapshot", json!({}));
|
||||
assert_eq!(snapshot["provider"], "grok_bot");
|
||||
assert_eq!(snapshot["driver"], "grok_bot_mcp");
|
||||
let state = recovered.state.as_ref().unwrap();
|
||||
let mut descriptor = serde_json::to_value(&state.descriptor).unwrap();
|
||||
descriptor["kind"] = json!("openai_dot");
|
||||
let tools = state.tools.authorized_tools().cloned().collect::<Vec<_>>();
|
||||
let catalog = AuthorizedToolSet {
|
||||
schema: "paperclip.runner.authorized-tools.v1".into(),
|
||||
schema_version: 1,
|
||||
catalog_digest: authorized_tool_catalog_digest(&tools).unwrap(),
|
||||
operations: tools,
|
||||
};
|
||||
let changed = json!({"provider":descriptor,"completionContract":state.completion_contract,"authorizedTools":catalog});
|
||||
let error = recovered
|
||||
.execute(&command("run.prepare", changed))
|
||||
.unwrap_err();
|
||||
assert!(error
|
||||
.to_string()
|
||||
.contains("immutable provider authority changed"));
|
||||
let revoked = call(&mut recovered, "run.cancel", json!({}));
|
||||
assert_eq!(revoked["externalStopConfirmed"], false);
|
||||
assert!(recovered
|
||||
.execute(&command(
|
||||
"external_provider.operation",
|
||||
operation("late", "progress", json!({"text":"late"}))
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renewal_is_durable_bounded_and_cannot_revive_fenced_authority() {
|
||||
let dir = TestDirectory::new();
|
||||
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: None,
|
||||
agent_runtime_version: None,
|
||||
command_digest:
|
||||
"sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
"sha256:a4c7e955df18bf2448224ecce2120b43ede8054fe00e7b33b4044b8bd284b321",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"claude" => AcpxReleaseProfile {
|
||||
|
||||
@@ -175,10 +175,9 @@ impl NativeProviderCommandExecutor {
|
||||
)
|
||||
})?;
|
||||
self.selected = Some(match kind {
|
||||
"openai_dot" => SelectedExecutor::Dot(DotCommandExecutor::with_runner_config(
|
||||
&self.state_dir,
|
||||
&self.config,
|
||||
)),
|
||||
"openai_dot" | "grok_bot" => SelectedExecutor::Dot(
|
||||
DotCommandExecutor::with_runner_config(&self.state_dir, &self.config),
|
||||
),
|
||||
"codex" | "opencode" => SelectedExecutor::LocalFacade(
|
||||
CodexCommandExecutor::with_runner_config(&self.state_dir, &self.config),
|
||||
),
|
||||
|
||||
@@ -39,7 +39,7 @@ const { commandDigest, ...attestation } = contract;
|
||||
assert.equal(commandDigest, `sha256:${createHash("sha256").update(canonicalJson(attestation)).digest("hex")}`);
|
||||
assert.equal(contract.acpxPatchSha256, createHash("sha256")
|
||||
.update(await readFile(new URL("../../patches/acpx@0.13.1.patch", root))).digest("hex"));
|
||||
for (const [agent, path] of [["pi", `test-fixtures/pi-acp/profile-v${manifest.profiles.pi.agentProfileVersion}-identity.json`], ["copilot", "test/fixtures/copilot-profile-v18-identity.json"]]) {
|
||||
for (const [agent, path] of [["pi", `test-fixtures/pi-acp/profile-v${manifest.profiles.pi.agentProfileVersion}-identity.json`], ["copilot", `test/fixtures/copilot-profile-v${manifest.profiles.copilot.agentProfileVersion}-identity.json`]]) {
|
||||
const identity = await readJson(path);
|
||||
assert.equal(manifest.profiles[agent].agentProfileVersion, identity.declaration.agentProfileVersion);
|
||||
assert.equal(manifest.profiles[agent].commandDigest, identity.commandDigest);
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
import type { CodexAppServerTransport } from "../drivers/codex/app-server-transport.js";
|
||||
import { CodexAppServerDriver } from "../drivers/codex/codex-app-server-driver.js";
|
||||
import type { CodexWorkingDirectoryAuthority } from "../drivers/codex/codex-boundaries.js";
|
||||
import { describeRunnerdDotDriver } from "../drivers/dot/runnerd-dot-driver.js";
|
||||
import { describeRunnerdExternalBotDriver } from "../drivers/dot/runnerd-dot-driver.js";
|
||||
import { HarnessDriverBackend } from "./harness-driver-backend.js";
|
||||
import {
|
||||
nativeSystemInstructions,
|
||||
@@ -209,8 +209,8 @@ export function describeRunnerdNativeSessionBackend(
|
||||
input: NativeExecutionInput,
|
||||
): Promise<NativeSessionBackendDescriptor> {
|
||||
if (input.schema === "paperclip.native-execution-input.v6") {
|
||||
// Dot uses its dedicated Rust bridge, rather than the JSON-RPC facade.
|
||||
const descriptor = describeRunnerdDotDriver();
|
||||
// External bots use the shared Rust bridge and retain their provider identity.
|
||||
const descriptor = describeRunnerdExternalBotDriver(input.provider.kind);
|
||||
return Promise.resolve({
|
||||
kind: "runner",
|
||||
name: descriptor.kind,
|
||||
|
||||
@@ -154,6 +154,7 @@ export class HarnessDriverBackend implements NativeSessionBackend {
|
||||
};
|
||||
const recoveryOptions: HarnessSessionRecoveryOptions = {
|
||||
signal: options.signal,
|
||||
normalizeReplayPayload: options.normalizeReplayPayload,
|
||||
};
|
||||
const recovered = await this.#driver.recoverSession(
|
||||
persisted,
|
||||
@@ -376,11 +377,12 @@ function assertProviderSessionIdentity(
|
||||
stage: "session.open" | "session.recover",
|
||||
): void {
|
||||
const ids = session.ids();
|
||||
const external = provider === "openai_dot_mcp" || provider === "grok_bot_mcp";
|
||||
if (
|
||||
typeof ids.driverSessionId !== "string" ||
|
||||
ids.driverSessionId.trim().length === 0 ||
|
||||
(provider !== "openai_dot_mcp" && (typeof ids.providerSessionId !== "string" || ids.providerSessionId.trim().length === 0))
|
||||
|| (provider === "openai_dot_mcp" && ids.providerSessionId !== null)
|
||||
(!external && (typeof ids.providerSessionId !== "string" || ids.providerSessionId.trim().length === 0))
|
||||
|| (external && ids.providerSessionId !== null)
|
||||
) {
|
||||
throw new Error(
|
||||
`provider_initialize_protocol_error: provider=${provider} stage=${stage} missing durable provider session identity`,
|
||||
|
||||
@@ -53,7 +53,7 @@ export function createNativeSessionBackend(
|
||||
options: NativeBackendFactoryOptions = {},
|
||||
): NativeSessionBackend {
|
||||
if (input.schema === "paperclip.native-execution-input.v6") {
|
||||
if (!options.dotRunnerOptions) throw new Error("Dot requires an admitted broker port and Rust Runner authority");
|
||||
if (!options.dotRunnerOptions) throw new Error("External bots require an admitted broker port and Rust Runner authority");
|
||||
return new HarnessDriverBackend(new RunnerdDotDriver({ ...options.dotRunnerOptions, execution: input,
|
||||
dynamicTools: options.dynamicTools, dynamicToolHandler: options.dynamicToolHandler,
|
||||
completionFeedback: options.completionFeedback, onSpawn: options.onSpawn }));
|
||||
|
||||
@@ -27,11 +27,12 @@ export function nativeSystemInstructions(input: NativeExecutionInput): string {
|
||||
throw new Error("native_runtime_context_entry_outside_bundle");
|
||||
}
|
||||
const entry = readFileSync(entryPath, "utf8");
|
||||
if (input.provider.kind === "openai_dot") {
|
||||
if (input.schema === "paperclip.native-execution-input.v6") {
|
||||
const grok = input.provider.kind === "grok_bot";
|
||||
return [input.runtimeContext.prompt.text, entry.trim(),
|
||||
"This provider has no mounted workspace. Use Paperclip semantic tools for task coordination and write_document for durable text deliverables. Read pinned skills with list_assigned_skills and read_assigned_skill. Assigned app tools run through the Paperclip MCP gateway with normal permissions and approvals. If workspace tools are advertised, use them for files and sandboxed commands, then register_deliverable for requested downloadable files. Use get_identity and list_people to discover the responsible person and assignees. Read the current catalog before concluding a capability is unavailable.",
|
||||
grok ? "Grok Bot has no Paperclip filesystem or shell access. Use first-party Paperclip tools for authoritative task context, progress, questions, documents and deliverables. Use your own native apps and tools under their existing permissions. Provider memory persists; retrieve current task instructions for each work session." : "This provider has no mounted workspace. Use Paperclip semantic tools for task coordination and write_document for durable text deliverables. Read pinned skills with list_assigned_skills and read_assigned_skill. Assigned app tools run through the Paperclip MCP gateway with normal permissions and approvals. If workspace tools are advertised, use them for files and sandboxed commands, then register_deliverable for requested downloadable files. Use get_identity and list_people to discover the responsible person and assignees. Read the current catalog before concluding a capability is unavailable.",
|
||||
// Keep tool-use guidance aligned with shared external-agent onboarding; the standalone Runner has no shared-package dependency.
|
||||
"When you receive a Paperclip assignment or message, use your own available tools, apps, plugins, skills, and capabilities to complete the requested work, then return the result to Paperclip. For example, use your existing Slack connection when the task calls for Slack. Paperclip's app catalog describes what this connection exposes; it is not a list of everything you can use in your own runtime. A missing Paperclip app connection does not make your own connection unavailable. Respect each tool's existing permissions, approvals, and the user's current instructions. Keep Paperclip data and actions within this connection's authorized company, agent, and task. Never use another Paperclip connection or identity to bypass a denial. Setup or test restrictions apply only to their stated scope; do not carry them into later requests unless the user explicitly says they continue.\n\nUse the assignment catalog through paperclip_dot_tool for Paperclip operations. Use your native tools directly for the rest of the work; they do not need to appear in that catalog. Record useful results and deliverables on the task and follow the Paperclip completion instructions.",
|
||||
"When you receive a Paperclip assignment or message, use your own available tools, apps, plugins, skills, and capabilities to complete the requested work, then return the result to Paperclip. For example, use your existing Slack connection when the task calls for Slack. Paperclip's app catalog describes what this connection exposes; it is not a list of everything you can use in your own runtime. A missing Paperclip app connection does not make your own connection unavailable. Respect each tool's existing permissions, approvals, and the user's current instructions. Keep Paperclip data and actions within this connection's authorized company, agent, and task. Never use another Paperclip connection or identity to bypass a denial. Setup or test restrictions apply only to their stated scope; do not carry them into later requests unless the user explicitly says they continue.\n\nUse the assignment catalog through paperclip_dot_tool for Paperclip operations. Use your native tools directly for the rest of the work; they do not need to appear in that catalog. Record useful results and deliverables on the task and follow the Paperclip completion instructions.".replaceAll("paperclip_dot_", grok ? "paperclip_grok_" : "paperclip_dot_"),
|
||||
`Assigned Paperclip skills: ${input.runtimeContext.skills.map(skill => skill.runtimeName).join(", ") || "none"}.`].join("\n\n");
|
||||
}
|
||||
return composeNativeSystemInstructions(input.runtimeContext, entry);
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface CompleteControlPlaneRunInput {
|
||||
* runner package never imports the server, UI, or database that sits behind it.
|
||||
*/
|
||||
export interface ControlPlanePort {
|
||||
normalizeReplayPayload?(payload: PrpEvent["payload"]): PrpEvent["payload"];
|
||||
openRun(input: OpenControlPlaneRunInput): Promise<void>;
|
||||
loadSessionCheckpoint?(): Promise<PersistedNativeSession | null>;
|
||||
checkpointSession?(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { digestPaperclipSemanticContent } from "../semantic-tools/receipts.js";
|
||||
|
||||
export type ExternalBotProviderKind = "openai_dot" | "grok_bot";
|
||||
export const GROK_BRIDGE_REVISION = "grok-bot-mcp-v1" as const;
|
||||
export const DOT_BRIDGE_REVISION = "dot-mcp-v1" as const;
|
||||
export interface DotBindingSnapshot {
|
||||
bindingId: string;
|
||||
|
||||
@@ -57,6 +57,7 @@ export interface OpenHarnessSessionInput {
|
||||
}
|
||||
|
||||
export interface HarnessSessionRecoveryOptions {
|
||||
normalizeReplayPayload?(payload: PrpEvent["payload"]): PrpEvent["payload"];
|
||||
/** Abort provider recovery and release any not-yet-returned provider state. */
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { nativeTaskConstraints } from "../backends/runtime-context.js";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { QUALIFIED_ACPX_PROFILES } from "../drivers/acpx/qualified-profiles.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -408,6 +409,16 @@ describe("NativeExecutionInputV1", () => {
|
||||
})).toThrow("does not match");
|
||||
});
|
||||
|
||||
it("decodes saved Copilot v17 input after the current source declaration advances", () => {
|
||||
const historical = JSON.parse(readFileSync(new URL("../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
|
||||
const { permissionPolicy, qualificationStatus: _status, ...currentProfile } = QUALIFIED_ACPX_PROFILES.copilot;
|
||||
const provider = { kind: "acpx", agent: "copilot", model: "explicit-provider-model", permissionPolicy,
|
||||
profile: { ...currentProfile, agentProfileVersion: 17, commandDigest: historical.commandDigest } };
|
||||
const parsed = parseNativeExecutionInput({ ...input, session: { ...input.session, driverKind: "acpx_runtime" }, provider });
|
||||
expect(parsed.provider).toEqual(provider);
|
||||
expect(parseNativeExecutionInput(parsed)).toEqual(parsed);
|
||||
});
|
||||
|
||||
it.each(Object.values(QUALIFIED_ACPX_PROFILES))(
|
||||
"admits the current $agent profile through the native execution boundary",
|
||||
(declaration) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { resolveClaudeEffort } from "../drivers/acpx/claude-effort.js";
|
||||
import type { DotBindingSnapshot } from "./external-provider.js";
|
||||
import type { DotBindingSnapshot, ExternalBotProviderKind } from "./external-provider.js";
|
||||
import { QUALIFIED_ACPX_VERSION } from "../drivers/acpx/generated-profiles.js";
|
||||
import { isSupportedAcpxProfileVersion, type AcpxProfileVersion } from "../drivers/acpx/profile-compatibility.js";
|
||||
import { isProviderMode } from "./provider-mode.js";
|
||||
@@ -249,9 +249,9 @@ export interface NativeExecutionInputV7 extends Omit<NativeExecutionInputV5, "sc
|
||||
|
||||
export interface NativeExecutionInputV6 extends Omit<NativeExecutionInputV5, "schema" | "provider" | "workspace" | "session"> {
|
||||
schema: typeof NATIVE_EXECUTION_INPUT_SCHEMA_V6;
|
||||
provider: { kind: "openai_dot"; model: null; binding: DotBindingSnapshot };
|
||||
provider: { kind: ExternalBotProviderKind; model: null; binding: DotBindingSnapshot };
|
||||
workspace: { access: "none"; cwd: null; repoUrl: null; repoRef: null; branchName: null };
|
||||
session: Omit<NativeExecutionInputV5["session"], "driverKind"> & { driverKind: "openai_dot_mcp" };
|
||||
session: Omit<NativeExecutionInputV5["session"], "driverKind"> & { driverKind: "openai_dot_mcp" | "grok_bot_mcp" };
|
||||
}
|
||||
|
||||
export type NativeExecutionInput = NativeExecutionInputV1 | NativeExecutionInputV2 | NativeExecutionInputV3 | NativeExecutionInputV4 | NativeExecutionInputV5 | NativeExecutionInputV6 | NativeExecutionInputV7;
|
||||
@@ -343,7 +343,7 @@ function nullableText(value: unknown, path: string): string | null {
|
||||
* intentionally not an extensible metadata bag: new fields require a contract
|
||||
* revision and an explicit security review.
|
||||
*/
|
||||
function parseDotNativeExecutionInput(input: Record<string, unknown>): NativeExecutionInputV6 {
|
||||
function parseExternalNativeExecutionInput(input: Record<string, unknown>): NativeExecutionInputV6 {
|
||||
const provider = record(input.provider, "input.provider");
|
||||
exactKeys(provider, ["kind", "model", "binding"], "input.provider");
|
||||
const b = record(provider.binding, "input.provider.binding");
|
||||
@@ -352,16 +352,17 @@ function parseDotNativeExecutionInput(input: Record<string, unknown>): NativeExe
|
||||
const workspace = record(input.workspace, "input.workspace");
|
||||
exactKeys(workspace, ["access", "cwd", "repoUrl", "repoRef", "branchName"], "input.workspace");
|
||||
const session = record(input.session, "input.session");
|
||||
if (provider.kind !== "openai_dot" || provider.model !== null || workspace.access !== "none"
|
||||
const driverKind = provider.kind === "grok_bot" ? "grok_bot_mcp" : "openai_dot_mcp";
|
||||
if (!["openai_dot", "grok_bot"].includes(String(provider.kind)) || provider.model !== null || workspace.access !== "none"
|
||||
|| [workspace.cwd, workspace.repoUrl, workspace.repoRef, workspace.branchName].some(v => v !== null)
|
||||
|| session.driverKind !== "openai_dot_mcp" || record(session.lifecyclePolicy, "input.session.lifecyclePolicy").mode !== "per_turn"
|
||||
|| session.driverKind !== driverKind || record(session.lifecyclePolicy, "input.session.lifecyclePolicy").mode !== "per_turn"
|
||||
|| binding.companyId !== b.companyId || binding.agentId !== b.agentId
|
||||
|| !Number.isSafeInteger(b.bindingGeneration) || Number(b.bindingGeneration) < 1
|
||||
|| !Number.isSafeInteger(b.acceptByUnixMs) || !Number.isSafeInteger(b.expiresAtUnixMs)
|
||||
|| Number(b.acceptByUnixMs) < 1 || Number(b.acceptByUnixMs) >= Number(b.expiresAtUnixMs)
|
||||
|| Number(b.expiresAtUnixMs) - Number(b.acceptByUnixMs) > 86_400_000
|
||||
|| !Array.isArray(input.credentialBindings) || input.credentialBindings.length !== 0) {
|
||||
throw new NativeExecutionInputError("Dot v6 requires a bound per-turn remote service with no filesystem or credential access");
|
||||
throw new NativeExecutionInputError("External v6 requires a bound per-turn remote service with no filesystem or credential access");
|
||||
}
|
||||
// Reuse the preceding closed contract's task/context validation. The local
|
||||
// validation value never leaves this function or becomes an execution path.
|
||||
@@ -370,17 +371,17 @@ function parseDotNativeExecutionInput(input: Record<string, unknown>): NativeExe
|
||||
session: { ...session, driverKind: "codex_app_server" },
|
||||
workspace: { cwd: "/", repoUrl: null, repoRef: null, branchName: null } }) as NativeExecutionInputV5;
|
||||
return { ...common, schema: NATIVE_EXECUTION_INPUT_SCHEMA_V6,
|
||||
provider: { kind: "openai_dot", model: null, binding: {
|
||||
provider: { kind: provider.kind as ExternalBotProviderKind, model: null, binding: {
|
||||
bindingId: text(b.bindingId, "input.provider.binding.bindingId"), bindingGeneration: Number(b.bindingGeneration),
|
||||
companyId: text(b.companyId, "input.provider.binding.companyId"), agentId: text(b.agentId, "input.provider.binding.agentId"),
|
||||
acceptByUnixMs: Number(b.acceptByUnixMs), expiresAtUnixMs: Number(b.expiresAtUnixMs),
|
||||
} }, workspace: { access: "none", cwd: null, repoUrl: null, repoRef: null, branchName: null },
|
||||
session: { ...common.session, driverKind: "openai_dot_mcp" } };
|
||||
session: { ...common.session, driverKind } };
|
||||
}
|
||||
|
||||
export function parseNativeExecutionInput(value: unknown): NativeExecutionInput {
|
||||
const input = record(value, "input");
|
||||
if (input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V6) return parseDotNativeExecutionInput(input);
|
||||
if (input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V6) return parseExternalNativeExecutionInput(input);
|
||||
const isV7 = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7;
|
||||
const isV5 = isV7 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA;
|
||||
const isV4 = isV5 || input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V4;
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface OpenNativeSessionInput {
|
||||
}
|
||||
|
||||
export interface NativeSessionRecoveryOptions {
|
||||
normalizeReplayPayload?(payload: PrpEvent["payload"]): PrpEvent["payload"];
|
||||
/** Abort provider recovery and release any not-yet-returned provider state. */
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@ export interface NativeSessionCapabilities {
|
||||
queuedFollowUp?: boolean;
|
||||
interruption: boolean;
|
||||
structuredResult: boolean;
|
||||
/** Defaults to existing provider terminal guarantees when omitted. */
|
||||
completionBoundary?: "provider_termination" | "paperclip_work_session";
|
||||
/** External work-session completion does not prove a remote process exited. */
|
||||
providerTermination?: "confirmed" | "unavailable";
|
||||
providerTranscriptStreaming?: boolean;
|
||||
read?: boolean;
|
||||
reconciliation?: boolean;
|
||||
usage?: boolean;
|
||||
|
||||
@@ -112,6 +112,42 @@ it("renews one authenticated connection for three weeks without replacing its au
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it("reports how long the authenticated Runner has been disconnected", async () => {
|
||||
const root = mkdtempSync(resolve(tmpdir(), "runner-disconnect-age-"));
|
||||
const core = new DurablePrpControlPlane({ stateDirectory: root, identity, expectedRunnerVersion, expectedRunnerDigest });
|
||||
try {
|
||||
await core.start();
|
||||
expect(core.runnerDisconnectedForMs()).toBeNull();
|
||||
// An unauthenticated peer closing is not a Runner disconnect.
|
||||
let closeUnauthenticated = () => {};
|
||||
const unauthenticated = core.attachWireConnection({
|
||||
onJson: () => {},
|
||||
onClose: (listener) => {
|
||||
closeUnauthenticated = () => listener({ message: "closed" });
|
||||
},
|
||||
close: () => closeUnauthenticated(),
|
||||
sendJson: () => {},
|
||||
});
|
||||
expect(unauthenticated.isAuthenticated()).toBe(false);
|
||||
closeUnauthenticated();
|
||||
expect(core.runnerDisconnectedForMs()).toBeNull();
|
||||
const client = (await authenticate(core, core.issueBootstrapTicket()))!;
|
||||
expect(core.runnerDisconnectedForMs()).toBeNull();
|
||||
const before = Date.now();
|
||||
client.socket.destroy();
|
||||
await vi.waitFor(() => expect(core.activeRunnerConnectionCount()).toBe(0), { timeout: 1_000 });
|
||||
const after = Date.now();
|
||||
expect(core.runnerDisconnectedForMs(after + 300_000)).toBeGreaterThanOrEqual(300_000);
|
||||
expect(core.runnerDisconnectedForMs(after + 300_000)).toBeLessThanOrEqual(300_000 + after - before);
|
||||
const resumed = (await authenticate(core, client.leaseToken!))!;
|
||||
expect(core.runnerDisconnectedForMs()).toBeNull();
|
||||
resumed.socket.destroy();
|
||||
} finally {
|
||||
await core.stop();
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it.each(["expired", "revoked", "wrong-run", "wrong-connection", "wrong-epoch", "future-expiry"])(
|
||||
"cannot renew a lease with %s authority",
|
||||
async (fault) => {
|
||||
|
||||
@@ -1492,6 +1492,9 @@ export class DurablePrpControlPlane {
|
||||
#expectedRunnerDigest: string;
|
||||
#server: Server | null = null;
|
||||
#connections = new Set<AuthorityConnection>();
|
||||
// Set when the last authenticated Runner connection closes; cleared on the
|
||||
// next authentication. Diagnostic only: it never gates authority.
|
||||
#runnerDisconnectedAt: number | null = null;
|
||||
#connectionProcessing = new Map<AuthorityConnection, Promise<void>>();
|
||||
#pendingSemanticCalls = new Set<string>();
|
||||
#semanticCallbacksRetired = false;
|
||||
@@ -1654,6 +1657,15 @@ export class DurablePrpControlPlane {
|
||||
).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Milliseconds since the last authenticated Runner connection closed, or
|
||||
* null when a Runner is connected or has never authenticated.
|
||||
*/
|
||||
runnerDisconnectedForMs(now = Date.now()): number | null {
|
||||
if (this.#runnerDisconnectedAt === null || this.activeRunnerConnectionCount() > 0) return null;
|
||||
return Math.max(0, now - this.#runnerDisconnectedAt);
|
||||
}
|
||||
|
||||
/** A reusable close requires every admitted callback's exact durable result. */
|
||||
semanticToolResultsSettled(): boolean {
|
||||
return (
|
||||
@@ -2084,7 +2096,11 @@ export class DurablePrpControlPlane {
|
||||
};
|
||||
void tail.then(release, release);
|
||||
},
|
||||
onClose: () => this.#connections.delete(connection),
|
||||
onClose: () => {
|
||||
this.#connections.delete(connection);
|
||||
if (connection.secureChannel !== null && this.activeRunnerConnectionCount() === 0)
|
||||
this.#runnerDisconnectedAt ??= Date.now();
|
||||
},
|
||||
});
|
||||
this.#connections.add(connection);
|
||||
return {
|
||||
@@ -2746,6 +2762,7 @@ export class DurablePrpControlPlane {
|
||||
pending.serverProof,
|
||||
clientProof,
|
||||
);
|
||||
this.#runnerDisconnectedAt = null;
|
||||
for (const active of this.#connections) {
|
||||
if (active !== connection && active.secureChannel !== null)
|
||||
active.close();
|
||||
|
||||
@@ -87,16 +87,25 @@ describe("Copilot build-owned installation", () => {
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
|
||||
});
|
||||
|
||||
it("preserves v17 executable and policy identity while binding the shared Claude effort sources", () => {
|
||||
it("preserves v17 history while binding the shared Grok descriptor schema", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v17-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v18-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort())
|
||||
.toEqual(["agentProfileVersion", "semanticDirectDriverSourceSha256", "semanticSchemaBundleSourceSha256", "semanticSidecarProtocolSourceSha256", "semanticSidecarSourceSha256"]);
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual(["agentProfileVersion", "semanticSchemaBundleSourceSha256"]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
|
||||
});
|
||||
|
||||
it("admits the current v18 declaration and binds its source policy, receipts and patch hashes", () => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v18-identity.json", import.meta.url), "utf8"));
|
||||
it("retains merged v18 while binding Claude effort to the combined shared sources", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v18-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v19-identity.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort())
|
||||
.toEqual(["agentProfileVersion", "semanticDirectDriverSourceSha256", "semanticSchemaBundleSourceSha256", "semanticSidecarProtocolSourceSha256", "semanticSidecarSourceSha256"]);
|
||||
expect(Object.keys(current.declaration).sort()).toEqual(Object.keys(prior.declaration).sort());
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBe("pending");
|
||||
});
|
||||
it("admits the current v19 declaration and binds its source policy, receipts and patch hashes", () => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v19-identity.json", import.meta.url), "utf8"));
|
||||
expect(identity.declaration.systemInstructionDelivery).toBe(COPILOT_SYSTEM_INSTRUCTION_DELIVERY);
|
||||
expect(identity.declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1");
|
||||
expect(identity.declaration.permissionContextContract).toBe(COPILOT_PERMISSION_CONTEXT_CONTRACT);
|
||||
@@ -127,7 +136,7 @@ describe("Copilot build-owned installation", () => {
|
||||
expect([...closure].filter(path => !bound.has(path))).toEqual(["negative-control:unbound-policy"]);
|
||||
});
|
||||
it.each([...PERMISSION_POLICY_SOURCES, ...SEMANTIC_RECEIPT_SOURCES])("changing %s changes the candidate identity", (_path, field) => {
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v18-identity.json", import.meta.url), "utf8"));
|
||||
const identity = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v19-identity.json", import.meta.url), "utf8"));
|
||||
identity.declaration[field] = "0".repeat(64);
|
||||
const sorted = Object.fromEntries(Object.entries(identity.declaration).sort(([a], [b]) => a.localeCompare(b)));
|
||||
expect(`sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`).not.toBe(identity.commandDigest);
|
||||
@@ -168,7 +177,7 @@ describe("Copilot build-owned installation", () => {
|
||||
});
|
||||
it("rejects changed profile identities before native file access", async () => {
|
||||
vi.mocked(verifyNativeAcpxInstallation).mockClear();
|
||||
for (const override of [{ agentServerVersion: "latest" }, { commandDigest: "sha256:untrusted" }, { commandDigest: "sha256:ed39259990cb0efda48d6105d2fa3599aac76873eb75cd80b4d0b179ce0579a6" }, { agent: "cursor" }, { agentProfileVersion: 1 }, { agentProfileVersion: 2 }, { agentProfileVersion: 3 }, { agentProfileVersion: 4 }, { agentProfileVersion: 5 }, { agentProfileVersion: 6 }, { agentProfileVersion: 7 }, { agentProfileVersion: 8 }, { agentProfileVersion: 9 }, { agentProfileVersion: 10 }]) {
|
||||
for (const override of [{ agentServerVersion: "latest" }, { commandDigest: "sha256:untrusted" }, { commandDigest: "sha256:ed39259990cb0efda48d6105d2fa3599aac76873eb75cd80b4d0b179ce0579a6" }, { agent: "cursor" }, { agentProfileVersion: 1 }, { agentProfileVersion: 2 }, { agentProfileVersion: 3 }, { agentProfileVersion: 4 }, { agentProfileVersion: 5 }, { agentProfileVersion: 6 }, { agentProfileVersion: 7 }, { agentProfileVersion: 8 }, { agentProfileVersion: 9 }, { agentProfileVersion: 10 }, { agentProfileVersion: 17 }]) {
|
||||
await expect(verifyCopilotInstallation({ ...QUALIFIED_ACPX_PROFILES.copilot, ...override } as never)).rejects.toThrow("exact pinned");
|
||||
}
|
||||
expect(verifyNativeAcpxInstallation).not.toHaveBeenCalled();
|
||||
|
||||
@@ -48,12 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 18,
|
||||
"agentProfileVersion": 19,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
"commandDigest": "sha256:a4c7e955df18bf2448224ecce2120b43ede8054fe00e7b33b4044b8bd284b321",
|
||||
"qualificationStatus": "pending",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import { isSupportedAcpxProfileVersion } from "./profile-compatibility.js";
|
||||
|
||||
describe("historical ACPX profile decoding", () => {
|
||||
it("retains each provider's existing revision boundary", () => {
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 17)).toBe(true);
|
||||
expect(isSupportedAcpxProfileVersion("pi", 17)).toBe(true);
|
||||
expect(isSupportedAcpxProfileVersion("pi", 18)).toBe(true);
|
||||
expect(isSupportedAcpxProfileVersion("cursor", 15)).toBe(true);
|
||||
|
||||
@@ -8,7 +8,7 @@ export type AcpxProfileVersion = HistoricalAcpxProfileVersion | CurrentAcpxProfi
|
||||
|
||||
const historicalVersions: Readonly<Record<string, readonly HistoricalAcpxProfileVersion[]>> = {
|
||||
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
|
||||
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
|
||||
cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
};
|
||||
|
||||
|
||||
@@ -623,6 +623,7 @@ export class CodexAppServerDriver implements HarnessDriver {
|
||||
resumed: true,
|
||||
activeTurnId: recoveredActiveTurnId,
|
||||
semanticResult: snapshot.semanticResult ?? null,
|
||||
normalizeReplayPayload: options.normalizeReplayPayload,
|
||||
terminalTurns: snapshot.terminalTurns ?? [],
|
||||
codexUsageBaseline: snapshot.codexUsageBaseline,
|
||||
dispositionOnlyRecoveryConsumed,
|
||||
@@ -923,6 +924,7 @@ export class CodexAppServerDriver implements HarnessDriver {
|
||||
resumed: boolean;
|
||||
activeTurnId?: string | null;
|
||||
semanticResult?: PersistedHarnessSemanticResult | null;
|
||||
normalizeReplayPayload?: HarnessSessionRecoveryOptions["normalizeReplayPayload"];
|
||||
terminalTurns?: PersistedHarnessTurnTerminal[];
|
||||
codexUsageBaseline?: PersistedHarnessSession["codexUsageBaseline"];
|
||||
dispositionOnlyRecoveryConsumed?: boolean;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type CodexUsageBaseline, codexRunUsage } from "./codex-usage-baseline.js";
|
||||
import type {
|
||||
HarnessRuntimeRequest,
|
||||
HarnessSessionRecoveryOptions,
|
||||
HarnessThreadGoal,
|
||||
HarnessThreadLineageEntry,
|
||||
PersistedHarnessSemanticResult,
|
||||
@@ -112,6 +113,7 @@ export class CodexSessionState {
|
||||
codexUsageBaseline: CodexUsageBaseline | null = null;
|
||||
result: PrpStructuredRunResult | null = null;
|
||||
resultFingerprint: string | null = null;
|
||||
readonly normalizeReplayPayload?: HarnessSessionRecoveryOptions["normalizeReplayPayload"];
|
||||
resultCallId: string | null = null;
|
||||
resultTurnId: string | null = null;
|
||||
turnStartPending = false;
|
||||
@@ -158,6 +160,7 @@ export class CodexSessionState {
|
||||
resumed: boolean;
|
||||
activeTurnId?: string | null;
|
||||
semanticResult?: PersistedHarnessSemanticResult | null;
|
||||
normalizeReplayPayload?: HarnessSessionRecoveryOptions["normalizeReplayPayload"];
|
||||
terminalTurns?: PersistedHarnessTurnTerminal[];
|
||||
codexUsageBaseline?: CodexUsageBaseline;
|
||||
dispositionOnlyRecoveryConsumed?: boolean;
|
||||
@@ -214,6 +217,7 @@ export class CodexSessionState {
|
||||
structuredClone(input.opened.lineage),
|
||||
);
|
||||
}
|
||||
this.normalizeReplayPayload = input.normalizeReplayPayload;
|
||||
if (input.semanticResult) {
|
||||
const validation = validatePrpStructuredRunResult(
|
||||
input.semanticResult.result,
|
||||
|
||||
@@ -55,7 +55,10 @@ export function admitResult(
|
||||
): SemanticResultAdmission {
|
||||
const fingerprint = canonicalJson(result);
|
||||
if (state.resultFingerprint !== null) {
|
||||
return state.resultFingerprint === fingerprint ? "identical" : "conflict";
|
||||
const same = state.normalizeReplayPayload
|
||||
? replayFingerprint(state, JSON.parse(state.resultFingerprint)) === replayFingerprint(state, result)
|
||||
: state.resultFingerprint === fingerprint;
|
||||
return same ? "identical" : "conflict";
|
||||
}
|
||||
state.result = structuredClone(result);
|
||||
state.resultFingerprint = fingerprint;
|
||||
@@ -178,6 +181,11 @@ export function mapTerminalTurn(
|
||||
}
|
||||
}
|
||||
|
||||
function replayFingerprint(state: CodexSessionState, value: unknown): string {
|
||||
const payload = { value };
|
||||
return canonicalJson(state.normalizeReplayPayload?.(payload) ?? payload);
|
||||
}
|
||||
|
||||
function terminalFingerprint(
|
||||
state: CodexSessionState,
|
||||
turn: Record<string, unknown>,
|
||||
@@ -201,7 +209,9 @@ export function terminalReplayConflict(
|
||||
candidate !== null &&
|
||||
state.resultFingerprint !== null &&
|
||||
state.resultTurnId === turnId &&
|
||||
canonicalJson(candidate) !== state.resultFingerprint
|
||||
(state.normalizeReplayPayload
|
||||
? replayFingerprint(state, candidate) !== replayFingerprint(state, JSON.parse(state.resultFingerprint))
|
||||
: canonicalJson(candidate) !== state.resultFingerprint)
|
||||
) {
|
||||
return {
|
||||
code: "conflicting_semantic_result",
|
||||
@@ -211,9 +221,10 @@ export function terminalReplayConflict(
|
||||
const semanticResult = state.resultTurnId === turnId
|
||||
? state.result
|
||||
: candidate;
|
||||
if (
|
||||
terminalFingerprint(state, turn, semanticResult) !== expectedFingerprint
|
||||
) {
|
||||
const actualFingerprint = terminalFingerprint(state, turn, semanticResult);
|
||||
if (state.normalizeReplayPayload
|
||||
? replayFingerprint(state, JSON.parse(actualFingerprint)) !== replayFingerprint(state, JSON.parse(expectedFingerprint))
|
||||
: actualFingerprint !== expectedFingerprint) {
|
||||
return {
|
||||
code: "conflicting_turn_terminal",
|
||||
message: "changed from its committed terminal fingerprint",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { buildNativeModelEnvelope, parseNativeExecutionInput, type NativeExecuti
|
||||
import { NATIVE_RUNTIME_ASSET_SCHEMA, PAPERCLIP_EXECUTION_PROMPT, PAPERCLIP_EXECUTION_PROMPT_REVISION,
|
||||
nativeRuntimePromptDigest, canonicalNativeRuntimeContextDigest } from "../../contracts/runtime-context.js";
|
||||
|
||||
function execution(root: string): NativeExecutionInputV6 {
|
||||
function execution(root: string, provider: "openai_dot" | "grok_bot" = "openai_dot"): NativeExecutionInputV6 {
|
||||
const companyId = randomUUID(), agentId = randomUUID();
|
||||
const digest = "0".repeat(64);
|
||||
const context = {
|
||||
@@ -25,10 +25,10 @@ function execution(root: string): NativeExecutionInputV6 {
|
||||
schema: "paperclip.native-execution-input.v6",
|
||||
binding: { companyId, agentId, runId: randomUUID(), issueId: randomUUID(), executionWorkspaceId: randomUUID() },
|
||||
task: { identifier: "DOT-1", title: "Synthetic bridge recovery", description: null, prompt: "Read the synthetic counter.", workMode: "standard" },
|
||||
provider: { kind: "openai_dot", model: null, binding: { companyId, agentId, bindingId: randomUUID(), bindingGeneration: 1,
|
||||
provider: { kind: provider, model: null, binding: { companyId, agentId, bindingId: randomUUID(), bindingGeneration: 1,
|
||||
acceptByUnixMs: Date.now() + 600_000, expiresAtUnixMs: Date.now() + 7_200_000 } },
|
||||
workspace: { access: "none", cwd: null, repoUrl: null, repoRef: null, branchName: null },
|
||||
session: { normalizedSessionId: randomUUID(), driverKind: "openai_dot_mcp", protocolVersion: 1, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } },
|
||||
session: { normalizedSessionId: randomUUID(), driverKind: provider === "grok_bot" ? "grok_bot_mcp" : "openai_dot_mcp", protocolVersion: 1, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } },
|
||||
executionMode: "default", planningContext: null,
|
||||
completionContract: { id: randomUUID(), sha256: "sha256:" + digest, schemaVersion: "paperclip.completion-contract.v1",
|
||||
contract: { revision: "1", objective: "Read the counter", criteria: [{ id: "objective", requirement: "Read the counter" }] } },
|
||||
@@ -36,8 +36,8 @@ function execution(root: string): NativeExecutionInputV6 {
|
||||
}) as NativeExecutionInputV6;
|
||||
}
|
||||
|
||||
it("v6 closes Dot identity, workspace, model and credential fields", () => {
|
||||
const input = execution("/synthetic/pinned-instructions");
|
||||
it.each(["openai_dot", "grok_bot"] as const)("v6 closes %s identity, workspace, model and credential fields", provider => {
|
||||
const input = execution("/synthetic/pinned-instructions", provider);
|
||||
expect(parseNativeExecutionInput(input)).toEqual(input);
|
||||
expect(buildNativeModelEnvelope(input).workspace).toBeNull();
|
||||
expect(JSON.stringify(buildNativeModelEnvelope(input))).not.toContain("/synthetic");
|
||||
@@ -51,8 +51,8 @@ it("v6 closes Dot identity, workspace, model and credential fields", () => {
|
||||
]) expect(() => parseNativeExecutionInput(changed)).toThrow();
|
||||
});
|
||||
|
||||
it("admits the heartbeat descriptor with the real Dot capabilities and no broker or process startup", async () => {
|
||||
const input = execution("/synthetic/pinned-instructions");
|
||||
it.each(["openai_dot", "grok_bot"] as const)("admits the %s descriptor without a broker or process startup", async provider => {
|
||||
const input = execution("/synthetic/pinned-instructions", provider);
|
||||
const spawnSpy = vi.spyOn(controlPlane, "spawnRunner");
|
||||
try {
|
||||
const descriptor = await describeRunnerdNativeSessionBackend(input);
|
||||
@@ -70,8 +70,8 @@ it("admits the heartbeat descriptor with the real Dot capabilities and no broker
|
||||
});
|
||||
expect(descriptor).toEqual(await backend.descriptor());
|
||||
expect(descriptor).toMatchObject({
|
||||
kind: "runner", name: "openai_dot_mcp", version: "dot-mcp-v1",
|
||||
capabilities: { resume: false, steering: false, interruption: false, usage: false, dynamicTools: true },
|
||||
kind: "runner", name: provider === "grok_bot" ? "grok_bot_mcp" : "openai_dot_mcp", version: provider === "grok_bot" ? "grok-bot-mcp-v1" : "dot-mcp-v1",
|
||||
capabilities: { resume: false, steering: false, interruption: false, usage: false, dynamicTools: true, completionBoundary: "paperclip_work_session", providerTermination: "unavailable", providerTranscriptStreaming: false },
|
||||
runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" },
|
||||
});
|
||||
expect(spawnSpy).not.toHaveBeenCalled();
|
||||
@@ -120,7 +120,8 @@ it.each([
|
||||
} else {
|
||||
handle!.child.kill("SIGTERM");
|
||||
await handle!.completion;
|
||||
await expect(session.read!()).rejects.toThrow("dot_runner_process_exited_recovery_required");
|
||||
await expect(session.read!()).rejects.toThrow(
|
||||
/^dot_runner_process_exited_recovery_required: code=\S+ signal=\S+ disconnectedMs=(?:none|\d+)$/);
|
||||
}
|
||||
} finally {
|
||||
commandSpy.mockRestore(); spawnSpy.mockRestore();
|
||||
@@ -131,10 +132,10 @@ it.each([
|
||||
}
|
||||
}, 45000);
|
||||
|
||||
it.each(["local", "custom local directory", "external launcher"])("reattaches the %s Rust bridge without duplicate operations and refuses lost checkpoints", async launch => {
|
||||
it.each(["local", "custom local directory", "external launcher", "grok external launcher"])("reattaches the %s Rust bridge without duplicate operations and refuses lost checkpoints", async launch => {
|
||||
const root = await mkdtemp(join(tmpdir(), "dot-driver-recovery-"));
|
||||
await writeFile(join(root, "AGENTS.md"), "Use only the synthetic counter.");
|
||||
const input = execution(root);
|
||||
const input = execution(root, launch === "grok external launcher" ? "grok_bot" : "openai_dot");
|
||||
let send: ((op: ExternalProviderOperation) => Promise<void>) | undefined;
|
||||
let spawned: { pid: number; processGroupId: number | null; startedAt: string } | undefined;
|
||||
let calls = 0;
|
||||
@@ -159,7 +160,7 @@ it.each(["local", "custom local directory", "external launcher"])("reattaches th
|
||||
};
|
||||
const runnerState = launch === "local" ? join(root, "state/runner") : join(root, "external-runtime/runner");
|
||||
if (launch === "custom local directory") options.runnerStateDirectory = runnerState;
|
||||
if (launch === "external launcher") {
|
||||
if (["external launcher", "grok external launcher"].includes(launch)) {
|
||||
await mkdir(runnerState, { recursive: true });
|
||||
options.runnerStateDirectory = runnerState;
|
||||
options.readProviderState = async () => {
|
||||
@@ -190,6 +191,11 @@ it.each(["local", "custom local directory", "external launcher"])("reattaches th
|
||||
const call = operation("tool", { name: "synthetic_counter", arguments: {} });
|
||||
await send!(call);
|
||||
await vi.waitFor(() => expect(settlements.get(call.requestId)).toMatchObject({ status: "completed", isError: false }), { timeout: 10000 });
|
||||
if (input.provider.kind === "grok_bot") {
|
||||
const renewal = operation("renew", { expiresAtUnixMs: Date.now() + 3600000 });
|
||||
await send!(renewal);
|
||||
await vi.waitFor(() => expect(settlements.get(renewal.requestId)).toMatchObject({ status: "renewed" }), { timeout: 10000 });
|
||||
}
|
||||
const snapshot = await session.snapshot();
|
||||
expect(snapshot).toMatchObject({ providerSessionId: null, activeTurnId: options.identity.turnId, providerRecoveryPolicy: "same_session_only" });
|
||||
await session.detachControllerForRestart!();
|
||||
@@ -210,3 +216,9 @@ it.each(["local", "custom local directory", "external launcher"])("reattaches th
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 45000);
|
||||
|
||||
it("rejects a mismatched Grok driver before any provider state is attached", async () => {
|
||||
const input = execution("/synthetic/pinned-instructions", "grok_bot");
|
||||
expect(() => parseNativeExecutionInput({ ...input, session: { ...input.session, driverKind: "openai_dot_mcp" } })).toThrow();
|
||||
expect(() => parseNativeExecutionInput({ ...input, provider: { ...input.provider, kind: "unsupported_bot" } })).toThrow();
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
|
||||
import { resolve } from "node:path";
|
||||
import type { HarnessDriver, HarnessDriverDescriptor, HarnessSession, OpenHarnessSessionInput, PersistedHarnessSession } from "../../contracts/harness-driver.js";
|
||||
import type { NativeExecutionInputV6 } from "../../contracts/native-execution.js";
|
||||
import type { ExternalProviderPort } from "../../contracts/external-provider.js";
|
||||
import type { ExternalProviderPort, ExternalBotProviderKind } from "../../contracts/external-provider.js";
|
||||
import type { PrpEvent } from "../../protocol/replay-contract.js";
|
||||
import { validatePrpStructuredRunResult } from "../../protocol/replay-contract.js";
|
||||
import { DurablePrpControlPlane, spawnRunner, type RunnerProcessHandle } from "../../control-plane/durable-prp-control-plane.js";
|
||||
@@ -37,17 +37,22 @@ const capabilities = {
|
||||
dynamicTools: true, unsupported: ["resume", "steering", "interruption", "usage", "goals", "runtimeRequestResolution", "threadLineage"],
|
||||
};
|
||||
|
||||
/** The admission path can inspect Dot without allocating a broker port or Runner. */
|
||||
/** Inspect external capabilities without allocating a broker port or Runner. */
|
||||
export function describeRunnerdExternalBotDriver(provider: ExternalBotProviderKind): HarnessDriverDescriptor {
|
||||
const grok = provider === "grok_bot";
|
||||
return { kind: grok ? "grok_bot_mcp" : "openai_dot_mcp", displayName: grok ? "Grok Bot" : "OpenAI Dot", version: grok ? "grok-bot-mcp-v1" : "dot-mcp-v1", protocolVersion: "prp.v3",
|
||||
capabilities: { ...structuredClone(capabilities), completionBoundary: "paperclip_work_session", providerTermination: "unavailable", providerTranscriptStreaming: false }, runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" } };
|
||||
}
|
||||
|
||||
export function describeRunnerdDotDriver(): HarnessDriverDescriptor {
|
||||
return { kind: "openai_dot_mcp", displayName: "OpenAI Dot", version: "dot-mcp-v1", protocolVersion: "prp.v3",
|
||||
capabilities: structuredClone(capabilities), runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" } };
|
||||
return describeRunnerdExternalBotDriver("openai_dot");
|
||||
}
|
||||
|
||||
/** Thin SDK projection. Rust owns every lifecycle decision and durable receipt. */
|
||||
export class RunnerdDotDriver implements HarnessDriver {
|
||||
constructor(readonly options: RunnerdDotDriverOptions) {}
|
||||
async descriptor() {
|
||||
return describeRunnerdDotDriver();
|
||||
return describeRunnerdExternalBotDriver(this.options.execution.provider.kind);
|
||||
}
|
||||
async openSession(input: OpenHarnessSessionInput): Promise<HarnessSession> {
|
||||
if (input.runId !== this.options.identity.runId || input.normalizedSessionId !== this.options.identity.normalizedSessionId) {
|
||||
@@ -59,14 +64,18 @@ export class RunnerdDotDriver implements HarnessDriver {
|
||||
catch (error) { await session.detachControllerForRestart().catch(() => {}); throw error; }
|
||||
}
|
||||
async recoverSession(snapshot: PersistedHarnessSession, options: { signal: AbortSignal }) {
|
||||
if (snapshot.driverKind !== "openai_dot_mcp" || snapshot.runId !== this.options.identity.runId
|
||||
if (snapshot.driverKind !== describeRunnerdExternalBotDriver(this.options.execution.provider.kind).kind || snapshot.runId !== this.options.identity.runId
|
||||
|| snapshot.driverSessionId !== this.options.identity.normalizedSessionId || snapshot.providerSessionId != null) {
|
||||
return { recovered: false, reason: "Dot bridge checkpoint identity mismatch" };
|
||||
}
|
||||
let saved: Record<string, unknown> | null;
|
||||
try { saved = await readProviderCheckpoint(this.options); }
|
||||
catch { return { recovered: false, reason: "Dot bridge checkpoint invalid; external work requires reconciliation" }; }
|
||||
if (!saved) return { recovered: false, reason: "Dot bridge checkpoint missing; external work requires reconciliation" };
|
||||
if (!saved) return { recovered: false, reason: "External bridge checkpoint missing; external work requires reconciliation" };
|
||||
const descriptor = saved.descriptor as Record<string, unknown> | undefined;
|
||||
if (this.options.execution.provider.kind === "grok_bot" && descriptor?.kind !== "grok_bot") {
|
||||
return { recovered: false, reason: "External bridge checkpoint provider mismatch" };
|
||||
}
|
||||
if (saved.schema !== "paperclip.runner.dot-provider-state.v1" || saved.runId !== snapshot.runId || saved.sessionId !== snapshot.driverSessionId || saved.turnId !== this.options.identity.turnId) {
|
||||
return { recovered: false, reason: "Dot bridge checkpoint authority mismatch" };
|
||||
}
|
||||
@@ -83,6 +92,11 @@ async function readProviderCheckpoint(options: RunnerdDotDriverOptions): Promise
|
||||
return JSON.parse(readFileSync(path, "utf8"));
|
||||
}
|
||||
|
||||
export function runnerDisconnectDiagnostic(core: Pick<DurablePrpControlPlane, "runnerDisconnectedForMs">): string {
|
||||
const disconnectedMs = core.runnerDisconnectedForMs();
|
||||
return `disconnectedMs=${disconnectedMs ?? "none"}`;
|
||||
}
|
||||
|
||||
class RunnerdDotSession implements HarnessSession {
|
||||
#core: DurablePrpControlPlane | null = null;
|
||||
#process: RunnerProcessHandle | null = null;
|
||||
@@ -93,7 +107,7 @@ class RunnerdDotSession implements HarnessSession {
|
||||
#closed = false;
|
||||
#failure: Error | null = null;
|
||||
constructor(readonly options: RunnerdDotDriverOptions) {}
|
||||
ids() { return { driverSessionId: this.options.identity.normalizedSessionId, providerSessionId: null, displayId: "OpenAI Dot" }; }
|
||||
ids() { return { driverSessionId: this.options.identity.normalizedSessionId, providerSessionId: null, displayId: describeRunnerdExternalBotDriver(this.options.execution.provider.kind).displayName }; }
|
||||
|
||||
async open(signal?: AbortSignal) {
|
||||
const o = this.options;
|
||||
@@ -159,11 +173,13 @@ class RunnerdDotSession implements HarnessSession {
|
||||
// Remote monitors do not report an exit code. The authenticated,
|
||||
// persisted receipt is the evidence of shutdown, for either launcher.
|
||||
if (core.getCommand("dot_shutdown")?.status === "completed") return;
|
||||
this.#failure ??= new Error(`dot_runner_process_exited_recovery_required: code=${result.code} signal=${result.signal}`);
|
||||
// Without an exit status, the controller-side disconnect age separates
|
||||
// reconnect-grace expiry (the Runner was already away) from a crash.
|
||||
this.#failure ??= new Error(`dot_runner_process_exited_recovery_required: code=${result.code} signal=${result.signal} ${runnerDisconnectDiagnostic(core)}`);
|
||||
this.#wake();
|
||||
}, () => {
|
||||
if (this.#closed || core.getCommand("dot_shutdown")?.status === "completed") return;
|
||||
this.#failure ??= new Error("dot_runner_process_exited_recovery_required");
|
||||
this.#failure ??= new Error(`dot_runner_process_exited_recovery_required: ${runnerDisconnectDiagnostic(core)}`);
|
||||
this.#wake();
|
||||
});
|
||||
} else if (!await o.adoptExistingRunner.isAlive()) { throw new Error("dot_runner_adoption_failed"); }
|
||||
@@ -179,12 +195,16 @@ class RunnerdDotSession implements HarnessSession {
|
||||
if (checkpoint && (checkpoint.schema !== "paperclip.runner.dot-provider-state.v1"
|
||||
|| checkpoint.runId !== o.identity.runId || checkpoint.sessionId !== o.identity.normalizedSessionId
|
||||
|| checkpoint.turnId !== o.identity.turnId)) throw new Error("dot_runner_checkpoint_authority_mismatch");
|
||||
if (checkpoint && o.execution.provider.kind === "grok_bot"
|
||||
&& (checkpoint.descriptor as Record<string, unknown> | undefined)?.kind !== "grok_bot") {
|
||||
throw new Error("external_provider_checkpoint_provider_mismatch");
|
||||
}
|
||||
this.#detachPort = await o.port.attach(async operation => {
|
||||
await this.#command("external_provider.operation", { ...operation }, `dot_operation_${operation.requestId}`);
|
||||
}, () => this.interrupt(), !!checkpoint);
|
||||
await this.#command("run.prepare", {
|
||||
provider: { kind: "openai_dot", ...o.execution.provider.binding,
|
||||
instructions: `${nativeSystemInstructions(o.execution)}\n\nAccept the assignment before calling tools. Invoke paperclip_finish or paperclip_block, then submit the same structured result with paperclip_dot_finish. Keep request IDs stable across retries.` },
|
||||
provider: { kind: o.execution.provider.kind, ...o.execution.provider.binding,
|
||||
instructions: `${nativeSystemInstructions(o.execution)}\n\nAccept the assignment before calling tools. Invoke paperclip_finish or paperclip_block, then submit the same structured result with ${o.execution.provider.kind === "grok_bot" ? "paperclip_grok_finish" : "paperclip_dot_finish"}. Keep request IDs stable across retries.` },
|
||||
authorizedTools: authorizedToolSetForProvider(undefined, [...o.dynamicTools ?? [], ...codexSemanticToolSpecs()]),
|
||||
completionContract: { revision: o.execution.completionContract.contract.revision,
|
||||
criterionIds: o.execution.completionContract.contract.criteria.map(c => c.id) },
|
||||
@@ -238,7 +258,7 @@ class RunnerdDotSession implements HarnessSession {
|
||||
const proposed = this.#events.findLast(event => event.eventType === "run.result.proposed");
|
||||
const result = proposed ? validatePrpStructuredRunResult(proposed.payload) : null;
|
||||
const terminal = this.#events.findLast(event => event.eventType === "run.terminal");
|
||||
return { driverKind: "openai_dot_mcp", driverSessionId: this.ids().driverSessionId,
|
||||
return { driverKind: describeRunnerdExternalBotDriver(this.options.execution.provider.kind).kind, driverSessionId: this.ids().driverSessionId,
|
||||
providerSessionId: null, runId: this.options.identity.runId,
|
||||
normalizedSessionId: this.options.identity.normalizedSessionId,
|
||||
activeTurnId: typeof state.activeProviderTurnId === "string" ? state.activeProviderTurnId : null,
|
||||
|
||||
@@ -1787,12 +1787,52 @@ describe("Capability live runnerd and Codex session", () => {
|
||||
turnTimeoutMs: 500,
|
||||
});
|
||||
state.holdAfterTool = true;
|
||||
const killedTurn = captureTurnRejection(first.sendMessage("Apply idempotent progress once."));
|
||||
await vi.waitFor(async () => {
|
||||
expect((await firstStore.load(binding.sessionId))?.mockState).toContain(
|
||||
"Progress persisted through the live Codex tool loop.",
|
||||
);
|
||||
let turnSettled = false;
|
||||
let turnFailure: unknown = null;
|
||||
const killedTurn = captureTurnRejection(first.sendMessage("Apply idempotent progress once.")).then((error) => {
|
||||
turnSettled = true;
|
||||
turnFailure = error;
|
||||
return error;
|
||||
});
|
||||
try {
|
||||
await vi.waitFor(async () => {
|
||||
expect((await firstStore.load(binding.sessionId))?.mockState).toContain(
|
||||
"Progress persisted through the live Codex tool loop.",
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
const summarize = (snapshot: CapabilityLiveSessionSnapshot | null) => {
|
||||
if (snapshot === null) return null;
|
||||
const mock = JSON.parse(snapshot.mockState);
|
||||
return {
|
||||
revision: snapshot.revision,
|
||||
status: snapshot.status,
|
||||
activeTurnId: snapshot.activeTurnId,
|
||||
mockRevision: mock.revision,
|
||||
comments: mock.comments.length,
|
||||
evidenceKinds: snapshot.evidence.slice(-8).map((entry) => entry.kind),
|
||||
};
|
||||
};
|
||||
const stored = await firstStore.load(binding.sessionId).catch(() => null);
|
||||
const diagnostics = {
|
||||
turnSettled,
|
||||
turnFailure: turnFailure instanceof Error
|
||||
? { name: turnFailure.name, message: turnFailure.message.slice(0, 240) }
|
||||
: typeof turnFailure,
|
||||
requests: state.transports[0]?.requests.slice(-16).map((request) => request.method),
|
||||
memory: summarize(first.snapshot()),
|
||||
stored: summarize(stored),
|
||||
};
|
||||
if (error instanceof Error) error.message += `\nDurable progress diagnostics: ${JSON.stringify(diagnostics)}`;
|
||||
} catch (diagnosticError) {
|
||||
const diagnosticUnavailable = diagnosticError instanceof Error
|
||||
? { name: diagnosticError.name, message: diagnosticError.message.slice(0, 240) }
|
||||
: { name: typeof diagnosticError };
|
||||
if (error instanceof Error) error.message += `\nDurable progress diagnostics unavailable: ${JSON.stringify(diagnosticUnavailable)}`;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await expect(first.recordUsage({
|
||||
receiptId: "provider-response-1",
|
||||
providerResponseId: "response-1",
|
||||
|
||||
@@ -1404,6 +1404,61 @@ it("reserves a bounded suspension window after close preparation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ existingCommand: false, rejectLateRead: false },
|
||||
{ existingCommand: false, rejectLateRead: true },
|
||||
{ existingCommand: true, rejectLateRead: false },
|
||||
{ existingCommand: true, rejectLateRead: true },
|
||||
])("returns unconfirmed suspension at its deadline while a remote state read is still pending ($existingCommand, $rejectLateRead)", async ({ existingCommand, rejectLateRead }) => {
|
||||
vi.useFakeTimers();
|
||||
const commands: { commandId: string; type: string; status: string }[] = existingCommand
|
||||
? [{ commandId: "exact-suspend", type: "runner.suspend", status: "completed" }]
|
||||
: [];
|
||||
const readRunnerState = vi.fn(async () => {
|
||||
await new Promise<void>(resolveRead => setTimeout(resolveRead, 10_000));
|
||||
if (rejectLateRead) throw new Error("remote state unavailable after its own timeout");
|
||||
return { lifecycle: "suspended" };
|
||||
});
|
||||
const unhandled = vi.fn();
|
||||
process.on("unhandledRejection", unhandled);
|
||||
const queueSuspend = vi.fn((commandId: string) => commands.push({ commandId, type: "runner.suspend", status: "completed" }));
|
||||
const release = vi.fn();
|
||||
let settled = false;
|
||||
let suspended: boolean | undefined;
|
||||
const result = runnerdRecoveryInternals.awaitRunnerSuspensionBarrier({
|
||||
commands: () => commands,
|
||||
queueSuspend,
|
||||
readRunnerState,
|
||||
runnerHasExited: async () => true,
|
||||
pump: () => undefined,
|
||||
deadline: Date.now() + 2_500,
|
||||
pollIntervalMs: 0,
|
||||
}).then(value => {
|
||||
settled = true;
|
||||
suspended = value;
|
||||
if (value) release();
|
||||
return value;
|
||||
});
|
||||
try {
|
||||
await vi.advanceTimersByTimeAsync(2_500);
|
||||
expect(readRunnerState).toHaveBeenCalledOnce();
|
||||
expect(settled).toBe(true);
|
||||
expect(suspended).toBe(false);
|
||||
expect(queueSuspend).toHaveBeenCalledTimes(existingCommand ? 0 : 1);
|
||||
expect(release).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
try {
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
await expect(result).resolves.toBe(false);
|
||||
expect(release).not.toHaveBeenCalled();
|
||||
expect(unhandled).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
process.off("unhandledRejection", unhandled);
|
||||
vi.useRealTimers();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ provider: "acpx" as const, acpxAgent: "pi" as const, closeGraceMs: undefined, drained: true },
|
||||
{ provider: "codex" as const, acpxAgent: undefined, closeGraceMs: undefined, drained: false },
|
||||
@@ -2140,6 +2195,7 @@ it("denies the isolated Codex home without denying a remote execution workspace"
|
||||
readOnlyRoots: ["/usr/local"],
|
||||
});
|
||||
const serialized = args.join("\n");
|
||||
expect(args).not.toContain("project_doc_max_bytes=0");
|
||||
|
||||
expect(serialized).toContain(
|
||||
'"/workspaces/task/.paperclip-runtime/paperclip-runner/sessions/session/filesystem/codex-home"="none"',
|
||||
@@ -2830,12 +2886,14 @@ const fakeCodex = resolve(
|
||||
"../../runner/target/debug/fake-codex-app-server",
|
||||
);
|
||||
|
||||
it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooperative-release", "launch-failure"] as const)(
|
||||
it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooperative-release", "launch-failure", "launch-failure-sandbox", "launch-failure-unsandboxed"] as const)(
|
||||
"bounds setup registration teardown and retains unconfirmed state (%s)",
|
||||
async mode => {
|
||||
const root = await mkdtemp(join(tmpdir(), "runnerd-setup-registration-"));
|
||||
const runtimeDirectory = join(root, "native-state");
|
||||
await mkdir(runtimeDirectory, { mode: 0o700 });
|
||||
const launchFailure = mode.startsWith("launch-failure");
|
||||
let template: Record<string, unknown> | null = null;
|
||||
let registrationStarted!: () => void;
|
||||
const started = new Promise<void>(resolve => { registrationStarted = resolve; });
|
||||
let returnRegistration!: (registration: { connection: { mode: "listen"; listenAddress: string; listenPort: number; listenPath: string }; release: () => Promise<void> }) => void;
|
||||
@@ -2853,17 +2911,22 @@ it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooper
|
||||
};
|
||||
let registrationSignal: AbortSignal | undefined;
|
||||
const cleanup = vi.fn(async () => { await rm(runtimeDirectory, { recursive: true, force: true }); });
|
||||
const launch = vi.fn(() => { throw new Error("owned launcher failed before execution"); });
|
||||
const launch = vi.fn((spec: durableControlPlane.RunnerProcessLaunchSpec) => {
|
||||
expect(spec.environment.PAPERCLIP_RUNNER_EXTERNAL_SANDBOX).toBeUndefined();
|
||||
throw new Error("owned launcher failed before execution");
|
||||
});
|
||||
let settled = false;
|
||||
vi.useFakeTimers();
|
||||
const result = probeNativeRunnerEnvironment({
|
||||
runtimeDirectory, provider: "codex", model: "gpt-test", timeoutMs: 1_000,
|
||||
environment: { PATH: "", OPENAI_API_KEY: "inert-test-key" },
|
||||
environment: { PATH: "", OPENAI_API_KEY: "inert-test-key", PAPERCLIP_RUNNER_EXTERNAL_SANDBOX: "true" },
|
||||
transportOptions: {
|
||||
...(mode === "launch-failure-sandbox" ? { externallySandboxed: true } : mode === "launch-failure-unsandboxed" ? { externallySandboxed: false } : {}),
|
||||
runnerBinary: resolve(import.meta.dirname, "../../package.json"),
|
||||
codexCommand: resolve(import.meta.dirname, "../../package.json"),
|
||||
sourceCodexHome: "", runnerProcessLauncher: launch,
|
||||
controlPlaneRegistration: async (_authority, _identity, signal) => {
|
||||
template = structuredClone(_authority.store.state.runAttachTemplate!);
|
||||
registrationSignal = signal;
|
||||
if (mode === "cooperative-release") signal?.addEventListener("abort", () => returnRegistration(registered), { once: true });
|
||||
registrationStarted();
|
||||
@@ -2874,16 +2937,23 @@ it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooper
|
||||
}).then(value => { settled = true; return value; }, (error: unknown) => { settled = true; return error; });
|
||||
try {
|
||||
await started;
|
||||
if (mode === "launch-failure") returnRegistration(registered);
|
||||
if (launchFailure) returnRegistration(registered);
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
if (mode === "late-release" || mode === "late-release-error" || mode === "stuck-release") returnRegistration(registered);
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
if (mode === "late-release" || mode === "cooperative-release" || mode === "launch-failure") await result;
|
||||
if (mode === "late-release" || mode === "cooperative-release" || launchFailure) await result;
|
||||
expect(settled).toBe(true);
|
||||
expect(registrationSignal?.aborted).toBe(true);
|
||||
const failure = await result;
|
||||
expect(template).toMatchObject({ provider: { externallySandboxed: mode === "launch-failure-sandbox", readOnly: true, approvalPolicy: "never", collaborationMode: "plan" } });
|
||||
const args = (template!.provider as Record<string, unknown>).args as string[];
|
||||
expect(args).toContain("project_doc_max_bytes=0");
|
||||
expect(args[args.indexOf("project_doc_max_bytes=0") - 1]).toBe("-c");
|
||||
expect(args.indexOf("project_doc_max_bytes=0")).toBeLessThan(args.indexOf("app-server"));
|
||||
expect(args).not.toContain("--dangerously-bypass-approvals-and-sandbox");
|
||||
expect(args).toContain('permissions.paperclip-runner-workspace-read-only.network.enabled=false');
|
||||
expect(failure).toBeInstanceOf(Error);
|
||||
expect((failure as Error).message).toContain(mode === "launch-failure" ? "owned launcher failed before execution" : "Native provider hello probe timed out.");
|
||||
expect((failure as Error).message).toContain(launchFailure ? "owned launcher failed before execution" : "Native provider hello probe timed out.");
|
||||
if (mode === "stuck" || mode === "stuck-release" || mode === "late-release-error") {
|
||||
expect(failure).toBeInstanceOf(AggregateError);
|
||||
expect((failure as AggregateError).cause).toMatchObject({ message: "Native provider hello probe timed out." });
|
||||
@@ -2900,7 +2970,7 @@ it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooper
|
||||
expect(cleanup).toHaveBeenCalledOnce();
|
||||
await expect(stat(runtimeDirectory)).rejects.toThrow();
|
||||
}
|
||||
expect(launch).toHaveBeenCalledTimes(mode === "launch-failure" ? 1 : 0);
|
||||
expect(launch).toHaveBeenCalledTimes(launchFailure ? 1 : 0);
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
} finally {
|
||||
returnRegistration(registered);
|
||||
|
||||
@@ -782,6 +782,22 @@ async function awaitRunnerSuspensionBarrier(input: {
|
||||
deadline: number;
|
||||
pollIntervalMs?: number;
|
||||
}): Promise<boolean> {
|
||||
if (Date.now() >= input.deadline) return false;
|
||||
const readRunnerState = async (): Promise<Record<string, unknown> | undefined> => {
|
||||
const remainingMs = input.deadline - Date.now();
|
||||
if (remainingMs <= 0) return undefined;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
return await Promise.race([
|
||||
input.readRunnerState(),
|
||||
new Promise<undefined>(resolveDeadline => {
|
||||
timer = setTimeout(() => resolveDeadline(undefined), remainingMs);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
let existing = [...input.commands()]
|
||||
.reverse()
|
||||
.find(
|
||||
@@ -794,13 +810,16 @@ async function awaitRunnerSuspensionBarrier(input: {
|
||||
// Reuse its receipt only when the current exact durable state is already
|
||||
// suspended; otherwise issue a new command rather than waiting on history.
|
||||
try {
|
||||
if ((await input.readRunnerState()).lifecycle === "suspended")
|
||||
const state = await readRunnerState();
|
||||
if (state === undefined) return false;
|
||||
if (state.lifecycle === "suspended")
|
||||
return Date.now() < input.deadline;
|
||||
} catch {
|
||||
// The normal bounded barrier below handles unavailable state.
|
||||
}
|
||||
existing = undefined;
|
||||
}
|
||||
if (Date.now() >= input.deadline) return false;
|
||||
const commandId =
|
||||
existing?.commandId ??
|
||||
`command_close_suspend_${randomUUID().replaceAll("-", "")}`;
|
||||
@@ -820,11 +839,14 @@ async function awaitRunnerSuspensionBarrier(input: {
|
||||
}
|
||||
let lifecycle: unknown;
|
||||
try {
|
||||
lifecycle = (await input.readRunnerState()).lifecycle;
|
||||
const state = await readRunnerState();
|
||||
if (state === undefined) return false;
|
||||
lifecycle = state.lifecycle;
|
||||
} catch {
|
||||
// A remote filesystem can lag the command-result delivery by a small
|
||||
// amount. Keep the single close deadline as the fail-closed bound.
|
||||
}
|
||||
if (Date.now() >= input.deadline) return false;
|
||||
if (
|
||||
command?.status === "completed" &&
|
||||
lifecycle === "suspended" &&
|
||||
@@ -1264,6 +1286,7 @@ export interface CapabilityRunnerdCodexTransportOptions {
|
||||
runnerBinary?: string;
|
||||
codexCommand?: string;
|
||||
codexArgs?: string[];
|
||||
disableCodexProjectInstructions?: boolean;
|
||||
/** Controller-visible Codex home used only to seed the isolated runner home. */
|
||||
sourceCodexHome?: string | null;
|
||||
opencodeCommand?: string;
|
||||
@@ -3465,20 +3488,24 @@ export function createRunnerdCodexAppServerArgs(input: {
|
||||
codexCommand?: string;
|
||||
readOnlyRoots?: string[];
|
||||
instructionWorkingCopyRoot?: string;
|
||||
disableProjectInstructions?: boolean;
|
||||
}): string[] {
|
||||
// The filesystem policy denies HOME and CODEX_HOME to keep credentials and
|
||||
// runner state outside provider reach. Always bind those names to the actual
|
||||
// isolated runner home; a stale controller environment must never cause the
|
||||
// execution workspace itself to become an explicit deny root.
|
||||
return createIsolatedCodexAppServerArgs(
|
||||
{
|
||||
...input.environment,
|
||||
HOME: input.codexHome,
|
||||
CODEX_HOME: input.codexHome,
|
||||
},
|
||||
[...(input.readOnlyRoots ?? []), ...codexExecutableReadOnlyRoots(input.environment ?? {}, input.codexCommand)],
|
||||
input.instructionWorkingCopyRoot,
|
||||
);
|
||||
return [
|
||||
...(input.disableProjectInstructions ? ["-c", "project_doc_max_bytes=0"] : []),
|
||||
...createIsolatedCodexAppServerArgs(
|
||||
{
|
||||
...input.environment,
|
||||
HOME: input.codexHome,
|
||||
CODEX_HOME: input.codexHome,
|
||||
},
|
||||
[...(input.readOnlyRoots ?? []), ...codexExecutableReadOnlyRoots(input.environment ?? {}, input.codexCommand)],
|
||||
input.instructionWorkingCopyRoot,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export function unwrapToolResponse(response: Record<string, unknown>, preserveEnvelope = false): {
|
||||
@@ -5033,6 +5060,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
codexHome,
|
||||
codexCommand: effectiveCodexCommand,
|
||||
instructionWorkingCopyRoot: runtimeContext?.instructions.workingCopy?.rootPath,
|
||||
disableProjectInstructions: this.options.disableCodexProjectInstructions,
|
||||
readOnlyRoots: [
|
||||
...trustedRuntimeReadOnlyRoots(
|
||||
this.options.environment,
|
||||
@@ -5528,6 +5556,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
|
||||
codexHome,
|
||||
codexCommand: typeof recordedCommand === "string" ? recordedCommand : this.options.codexCommand,
|
||||
instructionWorkingCopyRoot: runtimeContext?.instructions.workingCopy?.rootPath,
|
||||
disableProjectInstructions: this.options.disableCodexProjectInstructions,
|
||||
readOnlyRoots: [
|
||||
...trustedRuntimeReadOnlyRoots(this.options.environment),
|
||||
...(runtimeContext
|
||||
@@ -7217,7 +7246,8 @@ export async function probeNativeRunnerEnvironment(options: {
|
||||
sourceCodexHome: sourceCodexHome ?? "",
|
||||
stateDirectory: options.runtimeDirectory,
|
||||
environment,
|
||||
externallySandboxed: false,
|
||||
externallySandboxed: options.transportOptions?.externallySandboxed === true,
|
||||
disableCodexProjectInstructions: true,
|
||||
closeGraceMs: 10_000,
|
||||
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
|
||||
});
|
||||
|
||||
@@ -7355,6 +7355,59 @@ describe("executeNativeSession recovery", () => {
|
||||
expect(completeRun).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each([false, true])("confirms a redacted completion after a lost acknowledgement (conflict: %s)", async conflict => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const completion = { ...result, summary: JSON.stringify({ operationId: "call_api", data: "private-fixture-value" }) };
|
||||
const history: PrpEvent[] = [];
|
||||
const normalize = (payload: PrpEvent["payload"]): PrpEvent["payload"] =>
|
||||
JSON.parse(JSON.stringify(payload).replaceAll("private-fixture-value", "***REDACTED***"));
|
||||
let committed!: () => void;
|
||||
const saved = new Promise<void>(resolve => { committed = resolve; });
|
||||
let lostAcknowledgement = false;
|
||||
const completeRun = vi.fn(async () => undefined);
|
||||
const session: NativeSession = {
|
||||
identity: () => identity,
|
||||
async capabilities() { return { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true }; },
|
||||
async startTurn() { return { turnId: "turn-recovery" }; },
|
||||
async *events() { yield runnerEvent(1, "turn.completed"); },
|
||||
async result() { return { result: completion, terminal, turnId: "turn-recovery" }; },
|
||||
async snapshot() { return { backendKind: "mock", sessionId: identity.sessionId, identity, activeTurnId: null }; },
|
||||
async close() {},
|
||||
};
|
||||
const port: ControlPlanePort = {
|
||||
async openRun() {}, async checkpointSession() {}, normalizeReplayPayload: normalize, completeRun,
|
||||
async appendEvent(event) {
|
||||
const row = event as PrpEvent;
|
||||
history.push({ ...row, payload: normalize(row.payload) });
|
||||
if (row.eventType === "run.result.accepted" && !lostAcknowledgement) {
|
||||
lostAcknowledgement = true;
|
||||
if (conflict) history.at(-1)!.payload = { ...history.at(-1)!.payload, changed: true };
|
||||
committed();
|
||||
return await new Promise<never>(() => undefined);
|
||||
}
|
||||
return { cursor: history.length, highestContiguousSourceSeq: row.sourceSeq, disposition: "committed" };
|
||||
},
|
||||
async replayEvents(request) {
|
||||
const events = history.filter(row => row.sourceInstanceId === request.sourceInstanceId && row.sourceSeq > request.afterSourceSeq);
|
||||
return { events, highestContiguousSourceSeq: events.at(-1)?.sourceSeq ?? request.afterSourceSeq };
|
||||
},
|
||||
};
|
||||
const execution = executeNativeSession({ input, backend: {
|
||||
async descriptor() { return { kind: "mock", name: "redacted-completion", version: "1", capabilities: await session.capabilities() }; },
|
||||
async openSession() { return session; },
|
||||
}, controlPlane: port, runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery", timeoutMs: 20 });
|
||||
const outcome = conflict ? expect(execution).rejects.toThrow("native_control_event_replay_conflict:1")
|
||||
: expect(execution).resolves.toMatchObject({ result: completion });
|
||||
await saved;
|
||||
await vi.advanceTimersByTimeAsync(25);
|
||||
await outcome;
|
||||
expect(completeRun).toHaveBeenCalledTimes(conflict ? 0 : 1);
|
||||
expect(JSON.stringify(history)).not.toContain("private-fixture-value");
|
||||
expect(history.filter(event => event.eventType === "run.result.accepted")).toHaveLength(1);
|
||||
} finally { vi.useRealTimers(); }
|
||||
});
|
||||
|
||||
it("keeps a reconstructed semantic result on its matched terminal turn", async () => {
|
||||
const semanticFingerprint = canonicalTestJson(result);
|
||||
const checkpoint: PersistedNativeSession = {
|
||||
|
||||
@@ -2062,6 +2062,7 @@ export async function executeNativeSession(
|
||||
operation: (signal) =>
|
||||
options.backend.recoverSession!(providerRecoveryCheckpoint, {
|
||||
signal,
|
||||
normalizeReplayPayload: options.controlPlane.normalizeReplayPayload?.bind(options.controlPlane),
|
||||
}),
|
||||
onLateResolution: async (lateRecovery) => {
|
||||
if (lateRecovery.session) {
|
||||
@@ -2761,7 +2762,8 @@ export async function executeNativeSession(
|
||||
if (
|
||||
expected === undefined ||
|
||||
existing.eventType !== expected.eventType ||
|
||||
canonicalJson(existing.payload) !== canonicalJson(expected.payload)
|
||||
canonicalJson(options.controlPlane.normalizeReplayPayload?.(existing.payload) ?? existing.payload)
|
||||
!== canonicalJson(options.controlPlane.normalizeReplayPayload?.(expected.payload) ?? expected.payload)
|
||||
) {
|
||||
throw new Error(
|
||||
`native_control_event_replay_conflict:${existing.sourceSeq}`,
|
||||
|
||||
@@ -28,7 +28,7 @@ export const callApiAction = {
|
||||
},
|
||||
"documentation": {
|
||||
"title": "Call the Paperclip API",
|
||||
"description": "Fallback only: call a discovered Paperclip API operation when dedicated tools lack the required operation or parameters. Uses your existing permissions. Prefer dedicated tools; never bypass a denial or runner lifecycle tool. For large text responses, read the returned artifact with GET /api/assets/{assetId}/content and responseText; follow nextOffsetBytes until null.",
|
||||
"description": "Fallback only: call a discovered Paperclip API operation when dedicated tools lack the required operation or parameters. Uses the same route permissions as agent REST access. Prefer dedicated tools for common workflows; route denials still apply. For large text responses, read the returned artifact with GET /api/assets/{assetId}/content and responseText; follow nextOffsetBytes until null.",
|
||||
"note": "Production HTTP fallback; does not grant privileges or replace dedicated tools."
|
||||
},
|
||||
"examples": {
|
||||
@@ -51,7 +51,7 @@ export const callApiAction = {
|
||||
"operationId": "call_api",
|
||||
"version": 1,
|
||||
"title": "Call the Paperclip API",
|
||||
"description": "Fallback only: call a discovered Paperclip API operation when dedicated tools lack the required operation or parameters. Uses your existing permissions. Prefer dedicated tools; never bypass a denial or runner lifecycle tool. For large text responses, read the returned artifact with GET /api/assets/{assetId}/content and responseText; follow nextOffsetBytes until null.",
|
||||
"description": "Fallback only: call a discovered Paperclip API operation when dedicated tools lack the required operation or parameters. Uses the same route permissions as agent REST access. Prefer dedicated tools for common workflows; route denials still apply. For large text responses, read the returned artifact with GET /api/assets/{assetId}/content and responseText; follow nextOffsetBytes until null.",
|
||||
"exposure": "optional",
|
||||
"requiredClaims": [
|
||||
"api:call"
|
||||
|
||||
@@ -1043,7 +1043,8 @@ export const providerDescriptorSchema = {
|
||||
"claude_managed",
|
||||
"aws_agentcore",
|
||||
"acpx",
|
||||
"openai_dot"
|
||||
"openai_dot",
|
||||
"grok_bot"
|
||||
]
|
||||
},
|
||||
"driver": {
|
||||
@@ -1053,7 +1054,8 @@ export const providerDescriptorSchema = {
|
||||
"claude_managed_agents_api",
|
||||
"aws_agentcore_harness_api",
|
||||
"acpx_runtime",
|
||||
"openai_dot_mcp"
|
||||
"openai_dot_mcp",
|
||||
"grok_bot_mcp"
|
||||
]
|
||||
},
|
||||
"model": {
|
||||
@@ -1078,7 +1080,8 @@ export const providerDescriptorSchema = {
|
||||
"enum": [
|
||||
"anthropic_managed_agents",
|
||||
"aws_bedrock_agentcore_harness",
|
||||
"openai_dot"
|
||||
"openai_dot",
|
||||
"grok_bot"
|
||||
]
|
||||
},
|
||||
"providerSessionId": {
|
||||
@@ -1309,6 +1312,19 @@ export const providerDescriptorSchema = {
|
||||
"const": "remote_service"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "grok_bot"
|
||||
},
|
||||
"driver": {
|
||||
"const": "grok_bot_mcp"
|
||||
},
|
||||
"executionKind": {
|
||||
"const": "remote_service"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1447,6 +1463,38 @@ export const providerDescriptorSchema = {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "grok_bot"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider"
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"service",
|
||||
"providerSessionId"
|
||||
],
|
||||
"properties": {
|
||||
"service": {
|
||||
"const": "grok_bot"
|
||||
},
|
||||
"model": {
|
||||
"const": null
|
||||
},
|
||||
"providerSessionId": {
|
||||
"const": null
|
||||
},
|
||||
"agentProcessId": {
|
||||
"const": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"$defs": {
|
||||
|
||||
@@ -30,6 +30,17 @@ describe("provider runtime descriptor", () => {
|
||||
expect(validatePrpEvent(event).ok).toBe(true);
|
||||
expect(validatePrpEvent({ ...event, payload: { ...event.payload, turnControls: { steering: "true" } } }).ok).toBe(false);
|
||||
});
|
||||
it.each(["openai_dot", "grok_bot"] as const)("accepts the real %s external-session descriptor and rejects native-process claims", provider => {
|
||||
const descriptor = { provider, driver: provider === "grok_bot" ? "grok_bot_mcp" : "openai_dot_mcp", service: provider,
|
||||
executionKind: "remote_service", model: null, providerSessionId: null, processId: null, providerVersion: "unknown" };
|
||||
expect(validate(descriptor), JSON.stringify(validate.errors)).toBe(true);
|
||||
for (const changed of [
|
||||
{ ...descriptor, service: "anthropic_managed_agents" }, { ...descriptor, driver: "codex_app_server" },
|
||||
{ ...descriptor, executionKind: "local_process" }, { ...descriptor, model: "an-unreported-model" },
|
||||
{ ...descriptor, providerSessionId: "invented-session" }, { ...descriptor, processId: 42 }, { ...descriptor, agentProcessId: 42 },
|
||||
]) expect(validate(changed)).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts ACPX sidecar and child process identities separately", () => {
|
||||
expect(validate({
|
||||
provider: "acpx",
|
||||
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 18,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "d2777bfc3e15ee7861ce57a069ffb55d3dc266364adf7915845bfbad5aa2e402",
|
||||
"semanticDirectDriverSourceSha256": "187772a17e5d4f854ec10f37abfcd294cf4f1db58a91d18e7645f4a8c617f329",
|
||||
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5",
|
||||
"semanticSchemaBundleSourceSha256": "fb4a232460dc9e6c7e7221cb4ddd66d7cb82fa0b35e1b3eb04f2d1ee5ee18c86",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"commandDigest": "sha256:bc0827bd362d01b5595738515f0ca1adcd6961ec622034cbda4633b0f5ec4701",
|
||||
"commandDigest": "sha256:2ec904f44218f51058fa83d7437b26c4551e1c65e6f2c55f5a6386fa4061e96d",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
@@ -30,14 +30,14 @@
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "d2777bfc3e15ee7861ce57a069ffb55d3dc266364adf7915845bfbad5aa2e402",
|
||||
"semanticDirectDriverSourceSha256": "187772a17e5d4f854ec10f37abfcd294cf4f1db58a91d18e7645f4a8c617f329",
|
||||
"semanticSidecarSourceSha256": "91629734a80f0d32295cbce2c573b32af9e345f45302ff4525c269f66811aa06",
|
||||
"semanticDirectDriverSourceSha256": "2b7b7eaadee658312816ecdcbcb311ce491521fedf39e2ecb571036c36ff7b8e",
|
||||
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5",
|
||||
"semanticSchemaBundleSourceSha256": "fb4a232460dc9e6c7e7221cb4ddd66d7cb82fa0b35e1b3eb04f2d1ee5ee18c86",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSchemaBundleSourceSha256": "969c1c47bc538422210e3789bd53249ebf0f6be335a38c7997d201f0783dfd6b",
|
||||
"semanticSidecarProtocolSourceSha256": "334b67857bd81e9924186890258cb2ad41f92b1ef68ec60fa5c5d9fbb277673c",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"commandDigest": "sha256:a4c7e955df18bf2448224ecce2120b43ede8054fe00e7b33b4044b8bd284b321",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 19,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "fe1bb611bea289d81b1c7c973fbb01b93de133a7caa8fe78b7ff4ee1f485b224",
|
||||
"permissionAdapterSourceSha256": "49421a779c76e286ce65d75052026eab9630b1995af9ccf045a2d43d284633ce",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"permissionIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "cf136da18e2cdbefae8e01d23d2c93f64eb7aebccaec938e84c2887c888f87ee",
|
||||
"toolEvidenceSourceSha256": "49d7d09eb6957e596a173b0afd763544afe582f0c1f05cc0992fe51916a8fdb3",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "d2777bfc3e15ee7861ce57a069ffb55d3dc266364adf7915845bfbad5aa2e402",
|
||||
"semanticDirectDriverSourceSha256": "187772a17e5d4f854ec10f37abfcd294cf4f1db58a91d18e7645f4a8c617f329",
|
||||
"semanticValidationSourceSha256": "3b0ac8d3f3cb92747d0bcea472ab0419a140e1cfdc43047e2c22542acc2d105f",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "fa2b0c81cb9a62965839c905db81b930794774d8d557a01bce077dae5830b594",
|
||||
"semanticValidatorsSourceSha256": "4f0219ed00938c5d8bc1b11532ecab9c13230012b23bfd6b7dd6b992e58e98d5",
|
||||
"semanticSchemaBundleSourceSha256": "e9ca8d1a7449fe28b63dcd0f858bfc233f1ec791e159cdd8436d6ea34a3089fe",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1"
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ import type { AiConnectionRouterRequest, AiConnectionRouterResult } from "@paper
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type { PluginContext } from "./types.js";
|
||||
import type { PluginContext, ResourceLifecycleEvent, ResourceLifecycleResult } from "./types.js";
|
||||
import type {
|
||||
PluginEnvironmentAcquireLeaseParams,
|
||||
PluginEnvironmentDestroyLeaseParams,
|
||||
@@ -360,6 +360,14 @@ export interface PluginDefinition {
|
||||
*/
|
||||
onAgentLifecycle?(params: AgentLifecycleRequest): Promise<AgentLifecycleResult>;
|
||||
|
||||
/**
|
||||
* Reconcile a committed resource event. Requires manifest resourceLifecycle and events.subscribe.
|
||||
* The host acknowledges successful results and replays interrupted/unfinished work.
|
||||
* Calls are at least once, in order per resource. Re-read current state and use
|
||||
* idempotent external operations; deletion events remain available after the row is gone.
|
||||
*/
|
||||
onResourceLifecycle?(event: ResourceLifecycleEvent): Promise<ResourceLifecycleResult>;
|
||||
|
||||
/** Propose a member from host-authorized candidates. Requires ai.connections.route. */
|
||||
onRouteAiConnection?(params: AiConnectionRouterRequest): Promise<AiConnectionRouterResult>;
|
||||
|
||||
|
||||
@@ -239,6 +239,7 @@ export type {
|
||||
PluginLocalFoldersClient,
|
||||
PluginEventsClient,
|
||||
ResourceLifecycleEvent,
|
||||
ResourceLifecycleResult,
|
||||
PluginJobsClient,
|
||||
PluginLaunchersClient,
|
||||
PluginHttpClient,
|
||||
|
||||
@@ -56,6 +56,7 @@ export type { PluginLauncherRenderContextSnapshot } from "@paperclipai/shared";
|
||||
import type {
|
||||
PluginEvent,
|
||||
ResourceLifecycleEvent,
|
||||
ResourceLifecycleResult,
|
||||
PluginIssueCheckoutOwnership,
|
||||
PluginIssueOrchestrationSummary,
|
||||
PluginIssueRelationSummary,
|
||||
@@ -1351,6 +1352,7 @@ export interface HostToWorkerMethods {
|
||||
handleApiRequest: [params: PluginApiRequestInput, result: PluginApiResponse];
|
||||
/** @see PLUGIN_SPEC.md §13.8 */
|
||||
agentLifecycle: [params: AgentLifecycleRequest, result: AgentLifecycleResult];
|
||||
resourceLifecycle: [params: ResourceLifecycleEvent, result: ResourceLifecycleResult];
|
||||
getData: [params: GetDataParams, result: unknown];
|
||||
/** @see PLUGIN_SPEC.md §13.9 */
|
||||
performAction: [params: PerformActionParams, result: unknown];
|
||||
@@ -1488,6 +1490,7 @@ export const HOST_TO_WORKER_OPTIONAL_METHODS: readonly HostToWorkerMethodName[]
|
||||
"handleWebhook",
|
||||
"handleApiRequest",
|
||||
"agentLifecycle",
|
||||
"resourceLifecycle",
|
||||
"getData",
|
||||
"performAction",
|
||||
"executeTool",
|
||||
|
||||
@@ -547,7 +547,8 @@ export interface PluginLocalFoldersClient {
|
||||
export interface PluginEventsClient {
|
||||
/** Read durable resource hooks. Requires events.subscribe and a company scope.
|
||||
* Creation is delivered first, then the remaining events in id order.
|
||||
* Page using afterId; reset it each polling sweep to retry failures and late commits.
|
||||
* Prefer manifest resourceLifecycle + onResourceLifecycle for automatic delivery.
|
||||
* Manual consumers page using afterId and restart pagination to revisit unacknowledged events.
|
||||
* Events repeat until acknowledged; use a company-scoped provider idempotency key.
|
||||
*/
|
||||
listLifecycle(companyId: string, limit?: number, afterId?: string): Promise<ResourceLifecycleEvent[]>;
|
||||
@@ -597,9 +598,18 @@ export type ResourceLifecycleEvent = {
|
||||
createdAt: string;
|
||||
} & (
|
||||
| { resourceType: "agent"; action: "create" | "pause" | "resume" | "terminate" }
|
||||
| { resourceType: "project"; action: "create" | "update" | "archive" }
|
||||
| { resourceType: "project"; action: "create" | "update" | "archive" | "delete" }
|
||||
);
|
||||
|
||||
/** Return acknowledged only after durable effects finish. Retry unfinished work;
|
||||
* defer when configuration is missing (replayed on configuration changes or startup).
|
||||
* Both retry and deferred leave the event unacknowledged.
|
||||
*/
|
||||
export interface ResourceLifecycleResult {
|
||||
eventId: string;
|
||||
status: "acknowledged" | "retry" | "deferred";
|
||||
}
|
||||
|
||||
/**
|
||||
* `ctx.jobs` — register handlers for scheduled jobs declared in the manifest.
|
||||
*
|
||||
|
||||
@@ -39,6 +39,7 @@ import { createPluginIdleDrain } from "./idle-drain.js";
|
||||
* @see PLUGIN_SPEC.md §14 — SDK Surface
|
||||
*/
|
||||
|
||||
import type { ResourceLifecycleEvent } from "./types.js";
|
||||
import fs from "node:fs";
|
||||
import { AsyncLocalStorage } from "node:async_hooks";
|
||||
import path from "node:path";
|
||||
@@ -1660,6 +1661,9 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
|
||||
return handleExecuteTool(params as ExecuteToolParams);
|
||||
case "detectExternalObjects":
|
||||
return handleDetectExternalObjects(params as DetectExternalObjectsParams);
|
||||
case "resourceLifecycle":
|
||||
if (!plugin.definition.onResourceLifecycle) throw methodNotImplemented("resourceLifecycle");
|
||||
return plugin.definition.onResourceLifecycle(params as ResourceLifecycleEvent);
|
||||
case "agentLifecycle":
|
||||
if (!plugin.definition.onAgentLifecycle) throw methodNotImplemented("agentLifecycle");
|
||||
return plugin.definition.onAgentLifecycle(params as AgentLifecycleRequest);
|
||||
@@ -1783,6 +1787,7 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
|
||||
if (plugin.definition.onIdleDrain) supportedMethods.push("prepareIdleSleep", "releaseIdleSleep");
|
||||
if (plugin.definition.onApiRequest) supportedMethods.push("handleApiRequest");
|
||||
if (plugin.definition.onDetectExternalObjects) supportedMethods.push("detectExternalObjects");
|
||||
if (plugin.definition.onResourceLifecycle) supportedMethods.push("resourceLifecycle");
|
||||
if (plugin.definition.onAgentLifecycle) supportedMethods.push("agentLifecycle");
|
||||
if (plugin.definition.onRouteAiConnection) supportedMethods.push("routeAiConnection");
|
||||
if (plugin.definition.onResolveExternalObject) supportedMethods.push("resolveExternalObject");
|
||||
|
||||
@@ -73,6 +73,38 @@ describe("isWorkerEntrypoint", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("worker resource lifecycle delivery", () => {
|
||||
it("advertises and dispatches the native lifecycle callback", async () => {
|
||||
const stdin = new PassThrough(), stdout = new PassThrough();
|
||||
const lines = createInterface({ input: stdout });
|
||||
const plugin = definePlugin({
|
||||
async setup() {},
|
||||
async onResourceLifecycle(event) { return { eventId: event.id, status: "retry" }; },
|
||||
});
|
||||
const worker = startWorkerRpcHost({ plugin, stdin, stdout });
|
||||
async function call(method: string, params: unknown) {
|
||||
const response = new Promise<JsonRpcResponse>(resolve => {
|
||||
const handler = (line: string) => {
|
||||
const message = parseMessage(line);
|
||||
if (isJsonRpcResponse(message)) { lines.off("line", handler); resolve(message); }
|
||||
};
|
||||
lines.on("line", handler);
|
||||
});
|
||||
stdin.write(serializeMessage(createRequest(method, params, method)));
|
||||
return response;
|
||||
}
|
||||
try {
|
||||
const initialized = await call("initialize", {
|
||||
manifest: { id: "test.lifecycle", apiVersion: 1, version: "1.0.0", displayName: "Lifecycle", description: "Test", author: "Tests", categories: ["automation"], capabilities: ["events.subscribe"], resourceLifecycle: ["project"], entrypoints: { worker: "worker.js" } },
|
||||
config: {}, databaseNamespace: null,
|
||||
});
|
||||
expect(initialized).toMatchObject({ result: { supportedMethods: expect.arrayContaining(["resourceLifecycle"]) } });
|
||||
expect(await call("resourceLifecycle", { id: "17", companyId: "company", resourceType: "project", resourceId: "project", action: "delete", createdAt: "2020-01-01T00:00:00Z" }))
|
||||
.toMatchObject({ result: { eventId: "17", status: "retry" } });
|
||||
} finally { worker.stop(); lines.close(); stdin.destroy(); stdout.destroy(); }
|
||||
});
|
||||
});
|
||||
|
||||
describe("worker performAction context", () => {
|
||||
it("does not derive context companyId from caller params without host actor context", async () => {
|
||||
const hostToWorker = new PassThrough();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/** Request intent only. Agents persist the selected adapter, not an automatic choice. */
|
||||
export type AgentRunnerChoice = "auto" | "paperclip" | "legacy";
|
||||
export type AgentRunner = Exclude<AgentRunnerChoice, "auto">;
|
||||
|
||||
@@ -13,12 +12,10 @@ export function paperclipRunnerProfileForHarness(harness: string): { provider: "
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Highest required GLIBC version in the shipped GNU/Linux daemon's ELF version needs. */
|
||||
export const PAPERCLIP_RUNNER_MINIMUM_GLIBC_VERSION = "2.39";
|
||||
|
||||
function supportsLinuxLibc(libc?: "glibc" | "musl" | "unknown", version?: string): boolean {
|
||||
if (libc !== undefined && libc !== "glibc") return false;
|
||||
// Source-only/platform callers do not assert a measured host libc version.
|
||||
if (version === undefined) return true;
|
||||
const match = typeof version === "string" ? version.trim().match(/^(?:glibc\s+)?(\d+)\.(\d+)(?:\.\d+)?$/) : null;
|
||||
if (!match) return false;
|
||||
@@ -28,15 +25,12 @@ function supportsLinuxLibc(libc?: "glibc" | "musl" | "unknown", version?: string
|
||||
return major > requiredMajor! || (major === requiredMajor && minor >= requiredMinor!);
|
||||
}
|
||||
|
||||
/** Creation defaults follow GNU/Linux and macOS artifacts; Alpine/musl is deferred. */
|
||||
export function paperclipRunnerSupportsPlatform(harness: string, platform: string, architecture: string, libc?: "glibc" | "musl" | "unknown", glibcVersion?: string): boolean {
|
||||
return !!paperclipRunnerProfileForHarness(harness)
|
||||
// Omitted libc preserves platform-only callers; detected unknown Linux is not qualified.
|
||||
&& ((platform === "linux" && supportsLinuxLibc(libc, glibcVersion) && (architecture === "x64" || architecture === "arm64"))
|
||||
|| (platform === "darwin" && (architecture === "arm64" || architecture === "x64")));
|
||||
}
|
||||
|
||||
/** Recover harness identity without changing execution or guessing unknown providers. */
|
||||
export function agentHarnessType(adapterType: string, config: Record<string, unknown> = {}): string {
|
||||
if (adapterType !== "paperclip_runner") return adapterType;
|
||||
if (config.provider === undefined || config.provider === "codex") return "codex_local";
|
||||
|
||||
@@ -100,5 +100,6 @@ import a98 from "./app-definitions/messages-api.json" with { type: "json" };
|
||||
import a99 from "./app-definitions/chat-completions-api.json" with { type: "json" };
|
||||
import a100 from "./app-definitions/local.json" with { type: "json" };
|
||||
import a101 from "./app-definitions/monday.json" with { type: "json" };
|
||||
import a102 from "./app-definitions/speko.json" with { type: "json" };
|
||||
import type { AppDefinition } from "./types/app-definition.js";
|
||||
export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35,a36,a37,a38,a39,a40,a41,a42,a43,a44,a45,a46,a47,a48,a49,a50,a51,a52,a53,a54,a55,a56,a57,a58,a59,a60,a61,a62,a63,a64,a65,a66,a67,a68,a69,a70,a71,a72,a73,a74,a75,a76,a77,a78,a79,a80,a81,a82,a83,a84,a85,a86,a87,a88,a89,a90,a91,a92,a93,a94,a95,a96,a97,a98,a99,a100,a101] as AppDefinition[];
|
||||
export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35,a36,a37,a38,a39,a40,a41,a42,a43,a44,a45,a46,a47,a48,a49,a50,a51,a52,a53,a54,a55,a56,a57,a58,a59,a60,a61,a62,a63,a64,a65,a66,a67,a68,a69,a70,a71,a72,a73,a74,a75,a76,a77,a78,a79,a80,a81,a82,a83,a84,a85,a86,a87,a88,a89,a90,a91,a92,a93,a94,a95,a96,a97,a98,a99,a100,a101,a102] as AppDefinition[];
|
||||
@@ -873,7 +873,7 @@ describe("AppDefinition catalog", () => {
|
||||
"ticktick",
|
||||
"xero",
|
||||
]);
|
||||
expect(APP_STORE_DEFINITIONS).toHaveLength(79);
|
||||
expect(APP_STORE_DEFINITIONS).toHaveLength(80);
|
||||
const connectableSlugs = new Set(
|
||||
CONNECTABLE_APP_DEFINITIONS.map((entry) => entry.slug),
|
||||
);
|
||||
|
||||
@@ -35,6 +35,7 @@ export const CONNECTABLE_APP_SLUGS = new Set([
|
||||
"github",
|
||||
"github-code-review-bot",
|
||||
"discord",
|
||||
"speko",
|
||||
"microsoft-teams",
|
||||
"telegram",
|
||||
"imessage-photon",
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"slug": "speko",
|
||||
"name": "Speko",
|
||||
"description": "Talk with a Paperclip agent through an experimental hosted voice connection.",
|
||||
"categories": [
|
||||
"communication"
|
||||
],
|
||||
"featured": false,
|
||||
"branding": {
|
||||
"logoUrl": "/brands/apps/speko.svg"
|
||||
},
|
||||
"urlPatterns": [
|
||||
"https://speko.ai/*",
|
||||
"https://platform.speko.ai/*"
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"key": "voice-agent",
|
||||
"label": "Talk with an agent",
|
||||
"purpose": "channel",
|
||||
"provider": "speko",
|
||||
"transport": "voice",
|
||||
"auth": "api_key",
|
||||
"ownershipModes": [
|
||||
"customer"
|
||||
],
|
||||
"whenToUse": "Use browser voice to start and continue work with a selected Paperclip agent.",
|
||||
"credentialFields": [
|
||||
{
|
||||
"key": "apiKey",
|
||||
"label": "Speko API key",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Paste your Speko API key",
|
||||
"secret": true
|
||||
},
|
||||
{
|
||||
"key": "agentId",
|
||||
"label": "Speko agent ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"placeholder": "agent_…",
|
||||
"secret": false
|
||||
}
|
||||
],
|
||||
"guidanceMd": "Create a dedicated Speko voice persona and choose its voice. Paperclip registers signed session tools and keeps the selected Paperclip agent responsible for task work. Public HTTPS callbacks are required. Review provider recording settings; Speko charges are separate from agent costs. Phone calling is withheld pending live qualification.",
|
||||
"consoleLinks": {
|
||||
"register": "https://platform.speko.ai/agents",
|
||||
"docs": "https://docs.speko.ai/guides/tool-calling"
|
||||
},
|
||||
"riskTier": "S3",
|
||||
"requiredResourceFilters": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -732,7 +732,7 @@ export type SecretProviderConfigHealthStatus =
|
||||
export const SECRET_STATUSES = ["active", "disabled", "archived", "deleted"] as const;
|
||||
export type SecretStatus = (typeof SECRET_STATUSES)[number];
|
||||
|
||||
export const SECRET_SCOPES = ["company", "user"] as const;
|
||||
export const SECRET_SCOPES = ["company", "user", "instance"] as const;
|
||||
export type SecretScope = (typeof SECRET_SCOPES)[number];
|
||||
|
||||
export const SECRET_MANAGED_MODES = ["paperclip_managed", "external_reference"] as const;
|
||||
@@ -1496,6 +1496,8 @@ export const PLUGIN_UI_SLOT_TYPES = [
|
||||
export type PluginUiSlotType = (typeof PLUGIN_UI_SLOT_TYPES)[number];
|
||||
|
||||
export const WORKSPACE_OVERVIEW_DEFAULT_LIMIT = 50;
|
||||
export const HEARTBEAT_RUN_LIST_DEFAULT_LIMIT = 200;
|
||||
export const HEARTBEAT_RUN_LIST_MAX_LIMIT = 1000;
|
||||
export const WORKSPACE_OVERVIEW_MAX_LIMIT = 100;
|
||||
export const WORKSPACE_OVERVIEW_LINKED_ISSUE_LIMIT = 4;
|
||||
|
||||
|
||||
@@ -3,3 +3,5 @@ export const EXTERNAL_AGENT_TOOL_GUIDANCE = `When you receive a Paperclip assign
|
||||
|
||||
/** The assignment catalog constrains the Paperclip bridge, not the external runtime. */
|
||||
export const DOT_AGENT_TOOL_GUIDANCE = `${EXTERNAL_AGENT_TOOL_GUIDANCE}\n\nUse the assignment catalog through paperclip_dot_tool for Paperclip operations. Use your native tools directly for the rest of the work; they do not need to appear in that catalog. Record useful results and deliverables on the task and follow the Paperclip completion instructions.`;
|
||||
|
||||
export const GROK_BOT_AGENT_TOOL_GUIDANCE = `${EXTERNAL_AGENT_TOOL_GUIDANCE}\n\nUse the current assignment catalog through paperclip_grok_tool for first-party Paperclip operations. Grok Bot retains its own connected apps and persistent memory. Retrieve current task context and instructions for each Paperclip work session; do not assume provider memory is isolated or reset. Acknowledge routine wakes using paperclip_grok_ack_wake. Accept admitted assignments before task operations and keep request IDs stable across retries. Report progress, questions and deliverables in Paperclip, then follow its canonical completion instructions. Routine acceptance, Paperclip session completion and remote process termination are distinct. Grok usage and spending are not reported.`;
|
||||
Loaded 100 of 1176 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user