Reconcile sandbox runtime hardening with current recovery
Combine task-scoped workspace adoption with chat isolation, bounded shutdown draining, and provider identity handling. Preserve per-run capability rotation evidence and the new master warm-session transition coverage. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
cd008e3444
|
|
@ -0,0 +1,50 @@
|
|||
// Run before building, and again inside the protected deployment job on reruns.
|
||||
module.exports = async function authorizeStorybookDeploy({ github, context }) {
|
||||
const fail = (message) => { throw new Error(message); };
|
||||
if (context.repo.owner !== "paperclipai" || context.repo.repo !== "paperclip") {
|
||||
fail("Storybook publishing is restricted to paperclipai/paperclip.");
|
||||
}
|
||||
if (context.eventName !== "workflow_dispatch" || !context.ref.startsWith("refs/heads/")) {
|
||||
fail("Storybook publishing requires a manual run from a repository branch.");
|
||||
}
|
||||
|
||||
// The selected branch must never be able to add itself to the allowlist.
|
||||
const { data: repository } = await github.rest.repos.get(context.repo);
|
||||
const { data: file } = await github.rest.repos.getContent({
|
||||
...context.repo,
|
||||
path: ".github/CODEOWNERS",
|
||||
ref: repository.default_branch,
|
||||
});
|
||||
if (file.encoding !== "base64" || typeof file.content !== "string") {
|
||||
fail("Cannot read the default branch CODEOWNERS file.");
|
||||
}
|
||||
const owners = new Set();
|
||||
for (const line of Buffer.from(file.content, "base64").toString("utf8").split(/\r?\n/)) {
|
||||
const fields = line.split("#", 1)[0].trim().split(/\s+/);
|
||||
for (const owner of fields.slice(1)) {
|
||||
// Individual GitHub accounts only. Teams/email entries do not grant access.
|
||||
if (/^@[a-z\d](?:[a-z\d-]*[a-z\d])?$/i.test(owner)) {
|
||||
owners.add(owner.slice(1).toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (owners.size === 0) fail("CODEOWNERS has no individual GitHub accounts.");
|
||||
for (const actor of [context.actor, process.env.GITHUB_TRIGGERING_ACTOR]) {
|
||||
if (!actor || !owners.has(actor.toLowerCase())) {
|
||||
fail(`Only default-branch CODEOWNERS may publish Storybook (${actor || "missing actor"}).`);
|
||||
}
|
||||
}
|
||||
|
||||
// A branch can edit its workflow. Require a GitHub-enforced CODEOWNER review
|
||||
// as well, so editing this check cannot grant an outsider deployment access.
|
||||
const { data: environment } = await github.rest.repos.getEnvironment({
|
||||
...context.repo,
|
||||
environment_name: "storybook-deploy",
|
||||
});
|
||||
const reviewers = environment.protection_rules
|
||||
?.find((rule) => rule.type === "required_reviewers")?.reviewers;
|
||||
if (environment.can_admins_bypass !== false || !reviewers?.length ||
|
||||
reviewers.some(({ type, reviewer }) => type !== "User" || !owners.has(reviewer.login.toLowerCase()))) {
|
||||
fail("storybook-deploy must require CODEOWNER reviewers and disable administrator bypass.");
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
const { execFileSync } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { storybookDestination, branchIndex } = require('./storybook-destination.cjs');
|
||||
|
||||
const destination = storybookDestination({
|
||||
branch: process.env.SOURCE_BRANCH, sha: process.env.SOURCE_SHA,
|
||||
runId: process.env.GITHUB_RUN_ID, runAttempt: process.env.GITHUB_RUN_ATTEMPT,
|
||||
bucket: process.env.STORYBOOK_S3_BUCKET, baseUrl: process.env.STORYBOOK_PUBLIC_BASE_URL,
|
||||
});
|
||||
const source = path.resolve('storybook-static');
|
||||
// Treat the artifact as public files, never as executable publisher code.
|
||||
function validateTree(dir) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.isSymbolicLink() || entry.name.startsWith('.') || (!entry.isDirectory() && !entry.isFile())) {
|
||||
throw new Error(`Unsupported artifact entry: ${path.join(dir, entry.name)}`);
|
||||
}
|
||||
if (entry.isDirectory()) validateTree(path.join(dir, entry.name));
|
||||
}
|
||||
}
|
||||
validateTree(source);
|
||||
for (const name of ['index.html', 'iframe.html', 'index.json']) {
|
||||
if (!fs.statSync(path.join(source, name)).isFile() || fs.statSync(path.join(source, name)).size === 0) {
|
||||
throw new Error(`Missing Storybook output: ${name}`);
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(path.join(source, 'deployment.json'), JSON.stringify(destination, null, 2) + '\n');
|
||||
const aws = (args) => execFileSync('aws', args, { stdio: 'inherit' });
|
||||
// Complete a unique build before changing the branch's entry point. No deletion
|
||||
// permissions, shared root writes or mixed-version branch assets are needed.
|
||||
aws(['s3', 'cp', source, `s3://${destination.bucket}/${destination.buildPrefix}/`,
|
||||
'--recursive', '--no-follow-symlinks', '--only-show-errors',
|
||||
'--cache-control', 'public,max-age=31536000,immutable']);
|
||||
const indexFile = path.join(process.env.RUNNER_TEMP, 'storybook-branch-index.html');
|
||||
fs.writeFileSync(indexFile, branchIndex(destination.buildUrl));
|
||||
aws(['s3', 'cp', indexFile, `s3://${destination.bucket}/${destination.prefix}/index.html`,
|
||||
'--content-type', 'text/html; charset=utf-8', '--cache-control', 'no-cache,max-age=0,must-revalidate', '--only-show-errors']);
|
||||
aws(['s3', 'cp', indexFile, `s3://${destination.bucket}/${destination.bookmarkPrefix}/index.html`,
|
||||
'--content-type', 'text/html; charset=utf-8', '--cache-control', 'no-cache,max-age=0,must-revalidate', '--only-show-errors']);
|
||||
const report = `[Branch Storybook](${destination.url})\n\n[This build](${destination.buildUrl})\n\nCommit: \`${destination.sha}\`\n`;
|
||||
const reportPath = path.join(process.env.RUNNER_TEMP, 'storybook-deployment.md');
|
||||
fs.writeFileSync(reportPath, report);
|
||||
if (process.env.GITHUB_OUTPUT) fs.appendFileSync(process.env.GITHUB_OUTPUT,
|
||||
`url=${destination.url}\nbuild_url=${destination.buildUrl}\nreport_path=${reportPath}\n`);
|
||||
if (process.env.GITHUB_STEP_SUMMARY) fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, report);
|
||||
console.log(JSON.stringify(destination));
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
const { createHash } = require('node:crypto');
|
||||
|
||||
function storybookDestination({ branch, sha, runId, runAttempt, bucket, baseUrl }) {
|
||||
if (typeof branch !== 'string' || !branch || /[\x00-\x20\x7f]/.test(branch)) {
|
||||
throw new Error('A non-empty repository branch name is required.');
|
||||
}
|
||||
if (!/^[a-f0-9]{40}$/.test(sha)) throw new Error('A full source commit SHA is required.');
|
||||
if (![runId, runAttempt].every((value) => /^[1-9]\d*$/.test(String(value)))) {
|
||||
throw new Error('A valid workflow run and attempt are required.');
|
||||
}
|
||||
if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket)) throw new Error('Invalid Storybook S3 bucket.');
|
||||
const base = new URL(baseUrl);
|
||||
if (base.protocol !== 'https:' || base.username || base.password || base.search || base.hash || base.pathname !== '/') {
|
||||
throw new Error('Storybook base URL must be a credential-free HTTPS origin.');
|
||||
}
|
||||
const label = branch.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0,60) || 'branch';
|
||||
const digest = createHash('sha256').update(branch).digest('hex').slice(0,16);
|
||||
const branchKey = `${label}-${digest}`;
|
||||
const prefix = `storybook/branches/${branchKey}`;
|
||||
const buildPrefix = `${prefix}/builds/${runId}-${runAttempt}`;
|
||||
// Use one reversible path segment: slashes and special characters become
|
||||
// ~HH UTF-8 bytes, so feature/foo and feature-foo never share a bookmark.
|
||||
let bookmarkKey = [...Buffer.from(branch)].map((byte) =>
|
||||
/[A-Za-z0-9_-]/.test(String.fromCharCode(byte))
|
||||
? String.fromCharCode(byte) : `~${byte.toString(16).toUpperCase().padStart(2, '0')}`).join('');
|
||||
// Reserve the existing hashed directories, including all immutable builds.
|
||||
bookmarkKey = bookmarkKey.replace(/-([a-f0-9]{16})$/, '~2D$1');
|
||||
// Keep arbitrarily long ref names within S3's object-key limit. ~long cannot
|
||||
// occur in the reversible encoding, whose escapes contain only hex digits.
|
||||
if (bookmarkKey.length > 900) bookmarkKey = `${bookmarkKey.slice(0, 800)}~long-${digest}`;
|
||||
const bookmarkPrefix = `storybook/branches/${bookmarkKey}`;
|
||||
return {
|
||||
branch, sha, bucket, branchKey, prefix, buildPrefix, bookmarkPrefix,
|
||||
url: `${base.origin}/${bookmarkPrefix}/`,
|
||||
legacyUrl: `${base.origin}/${prefix}/index.html`,
|
||||
buildUrl: `${base.origin}/${buildPrefix}/index.html`,
|
||||
};
|
||||
}
|
||||
|
||||
function branchIndex(buildUrl) {
|
||||
// The target is generated from a validated origin and ASCII path segments.
|
||||
const target = JSON.stringify(buildUrl).replace(/</g, '\\u003c');
|
||||
return `<!doctype html><html lang="en"><meta charset="utf-8"><title>Storybook preview</title>
|
||||
<script>const target = new URL(${target}); target.search = location.search; target.hash = location.hash; location.replace(target.href);</script>
|
||||
<noscript><a href="${buildUrl}">Open this branch's Storybook</a></noscript></html>\n`;
|
||||
}
|
||||
module.exports = { storybookDestination, branchIndex };
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { waitForCloudArtifacts } from "../../../scripts/cloud-readiness.mjs";
|
||||
import { previewManifest } from "../../../scripts/preview-artifacts.mjs";
|
||||
|
||||
const sha = "a".repeat(40);
|
||||
const digest = `sha256:${"b".repeat(64)}`;
|
||||
const json = (body, status = 200) => new Response(JSON.stringify(body), { status });
|
||||
function registry({ missing = new Set(), failure, wrongImage = false, wrongPackage = false } = {}) {
|
||||
return async (url) => {
|
||||
if (failure) return json({}, failure);
|
||||
if (url.startsWith("https://registry.npmjs.org/")) {
|
||||
const name = decodeURIComponent(new URL(url).pathname.split("/")[1]);
|
||||
if (missing.has(name.split("/")[1])) return json({}, 404);
|
||||
const pkg = previewManifest({ name, version: "0.0.0" }, sha);
|
||||
return json({ ...pkg, ...(wrongPackage ? { gitHead: "c".repeat(40) } : {}), dist: { integrity: "sha512-fixture", tarball: "https://registry.npmjs.org/fixture.tgz" } });
|
||||
}
|
||||
if (url.includes("/token?")) return json({ token: "fixture" });
|
||||
if (url.includes("/manifests/")) return missing.has("image") ? json({}, 404) : json({ config: { digest } });
|
||||
if (url.includes("/blobs/")) return json({ config: { Labels: { "org.opencontainers.image.revision": wrongImage ? "c".repeat(40) : sha } } });
|
||||
throw new Error(`Unexpected request: ${url}`);
|
||||
};
|
||||
}
|
||||
|
||||
test("readiness requires the image and both exact-source packages on the successful poll", async () => {
|
||||
const missing = new Set(["image", "shared", "db"]);
|
||||
let clock = 0;
|
||||
const states = [];
|
||||
const result = await waitForCloudArtifacts(sha, {
|
||||
fetchImpl: registry({ missing }), now: () => clock, intervalMs: 10, timeoutMs: 100, log: (message) => states.push(message),
|
||||
sleep: async (ms) => {
|
||||
clock += ms;
|
||||
if (clock === 10) missing.delete("image");
|
||||
if (clock === 20) missing.delete("shared");
|
||||
if (clock === 30) { missing.delete("db"); missing.add("image"); }
|
||||
if (clock === 40) missing.delete("image");
|
||||
},
|
||||
});
|
||||
assert.equal(clock, 40, "an artifact disappearing before the final poll must prevent readiness");
|
||||
assert.deepEqual(result, { version: 1, sha, packageVersion: `0.0.0-preview.g${sha}` });
|
||||
assert.match(states.at(-1), /Cloud artifacts available/);
|
||||
});
|
||||
|
||||
test("missing artifacts time out with a precise inventory and bounded sleep", async () => {
|
||||
let clock = 0;
|
||||
const sleeps = [];
|
||||
await assert.rejects(waitForCloudArtifacts(sha, {
|
||||
fetchImpl: registry({ missing: new Set(["db"]) }), now: () => clock, timeoutMs: 25, intervalMs: 20, log: () => {},
|
||||
sleep: async (ms) => { sleeps.push(ms); clock += ms; },
|
||||
}), /timed out.*missing: db/);
|
||||
assert.deepEqual(sleeps, [20, 5]);
|
||||
});
|
||||
|
||||
for (const fixture of [{ failure: 403 }, { failure: 503 }, { wrongImage: true }, { wrongPackage: true }]) {
|
||||
test(`registry errors and identity mismatches fail without waiting: ${JSON.stringify(fixture)}`, async () => {
|
||||
await assert.rejects(waitForCloudArtifacts(sha, {
|
||||
fetchImpl: registry(fixture), sleep: async () => assert.fail("must not retry an invalid artifact or upstream error"), log: () => {},
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
test("invalid source and timing configuration are rejected before registry access", async () => {
|
||||
const fetchImpl = async () => assert.fail("invalid inputs must not reach a registry");
|
||||
await assert.rejects(waitForCloudArtifacts("master", { fetchImpl }), /full immutable commit SHA/);
|
||||
for (const options of [{ timeoutMs: 0 }, { intervalMs: -1 }, { timeoutMs: Infinity }]) {
|
||||
await assert.rejects(waitForCloudArtifacts(sha, { ...options, fetchImpl }), /positive finite/);
|
||||
}
|
||||
});
|
||||
|
||||
test("the versioned readiness job requires successful source, image and artifact jobs", () => {
|
||||
const workflow = readFileSync(new URL("../../workflows/cloud-readiness.yml", import.meta.url), "utf8");
|
||||
assert.match(workflow, /push:\s*\n\s*branches: \[master\]/);
|
||||
assert.match(workflow, /group: cloud-readiness-\$\{\{ github.sha \}\}/);
|
||||
assert.match(workflow, /uses: \.\/\.github\/workflows\/release-verify.yml\s+with:\s+ref: \$\{\{ github.sha \}\}/);
|
||||
assert.match(workflow, /uses: \.\/\.github\/workflows\/docker-cloud.yml/);
|
||||
const ready = workflow.split(" ready:")[1];
|
||||
assert.match(ready, /name: Cloud deployable v1/);
|
||||
assert.match(ready, /needs: \[verify, image, artifacts\]/);
|
||||
assert.match(ready, /if: github.repository == 'paperclipai\/paperclip' && github.ref == 'refs\/heads\/master'/);
|
||||
assert.doesNotMatch(ready, /^\s*(?:if:.*always\(|continue-on-error:)/m);
|
||||
assert.doesNotMatch(workflow, /secrets: inherit|id-token: write|actions: write|checks: write|uses: .*@v\d\b/);
|
||||
const cloud = readFileSync(new URL("../../workflows/docker-cloud.yml", import.meta.url), "utf8");
|
||||
assert.doesNotMatch(cloud, /^ push:/m, "the master image must build only once");
|
||||
const migrator = readFileSync(new URL("../../workflows/cloud-artifacts.yml", import.meta.url), "utf8");
|
||||
assert.match(migrator, /push:\s*\n\s*branches: \[master\]/);
|
||||
assert.match(migrator, /SOURCE_SHA: \$\{\{ github.sha \}\}/);
|
||||
assert.match(migrator, /gh workflow run release.yml .*--ref master/);
|
||||
assert.match(migrator, /--field channel=cloud-migrator/);
|
||||
assert.match(migrator, /--field source_ref="\$SOURCE_SHA"/);
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { runInNewContext } from "node:vm";
|
||||
|
||||
const workflow = readFileSync(new URL("../../workflows/docker-cloud.yml", import.meta.url), "utf8");
|
||||
// Exercise the workflow's actual boolean expression. Its string comparisons and
|
||||
// boolean operators have the same results in JS for these canonical contexts.
|
||||
const expression = workflow.match(/^ runs-on: \$\{\{ (.+) \}\}$/m)?.[1];
|
||||
assert.ok(expression, "cloud routing must remain an explicit job expression");
|
||||
const timeoutExpression = workflow.match(/^ timeout-minutes: \$\{\{ (.+) \}\}$/m)?.[1];
|
||||
assert.ok(timeoutExpression, "AWS jobs must finish before the Fleet instance lifetime");
|
||||
const fleet = "runs-on/fleet=paperclip-cloud-build-x64/env=public-ci";
|
||||
const base = { repository: "paperclipai/paperclip", repository_id: "1170821064", ref: "refs/heads/master", event_name: "push" };
|
||||
for (const { name, github = {}, enabled = "true", expected = "ubuntu-latest" } of [
|
||||
{ name: "canonical master push", expected: fleet },
|
||||
{ name: "manual master build", github: { event_name: "workflow_dispatch" }, expected: fleet },
|
||||
{ name: "disabled switch", enabled: "false" },
|
||||
{ name: "missing switch", enabled: "" },
|
||||
{ name: "invalid switch", enabled: "yes" },
|
||||
{ name: "fork", github: { repository: "someone/paperclip", repository_id: "123" } },
|
||||
{ name: "wrong repository identity", github: { repository_id: "123" } },
|
||||
{ name: "pull request", github: { event_name: "pull_request", ref: "refs/pull/123/merge" } },
|
||||
{ name: "privileged PR event", github: { event_name: "pull_request_target" } },
|
||||
{ name: "release tag", github: { ref: "refs/tags/v2026.911.0" } },
|
||||
{ name: "branch push", github: { ref: "refs/heads/feature" } },
|
||||
{ name: "manual branch build", github: { event_name: "workflow_dispatch", ref: "refs/heads/feature" } },
|
||||
{ name: "workflow completion event", github: { event_name: "workflow_run" } },
|
||||
]) {
|
||||
test(`cloud runner routing: ${name}`, () => {
|
||||
const context = { github: { ...base, ...github }, vars: { AWS_CLOUD_BUILDS_ENABLED: enabled } };
|
||||
assert.equal(runInNewContext(expression, context), expected);
|
||||
assert.equal(runInNewContext(timeoutExpression, context), expected === fleet ? 40 : 60);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const workflow = readFileSync(new URL("../../workflows/docker-cloud.yml", import.meta.url), "utf8");
|
||||
const step = workflow.split(" - name: Free runner disk")[1].split(" - name: Login to GitHub Container Registry")[0];
|
||||
const script = step.split(" run: |\n")[1].split("\n").map((line) => line.replace(/^ {10}/, "")).join("\n");
|
||||
const threshold = 64 * 1024 * 1024;
|
||||
|
||||
for (const { name, dockerFree, workspaceFree, dfStatus = "0", infoStatus = "0", cleanup } of [
|
||||
{ name: "ample free space", dockerFree: threshold + 1, workspaceFree: threshold + 1, cleanup: false },
|
||||
{ name: "exactly the headroom threshold", dockerFree: threshold, workspaceFree: threshold, cleanup: false },
|
||||
{ name: "Docker filesystem below threshold", dockerFree: threshold - 1, workspaceFree: threshold + 1, cleanup: true },
|
||||
{ name: "workspace filesystem below threshold", dockerFree: threshold + 1, workspaceFree: threshold - 1, cleanup: true },
|
||||
{ name: "invalid Docker measurement", dockerFree: "unknown", workspaceFree: threshold + 1, cleanup: true },
|
||||
{ name: "invalid workspace measurement", dockerFree: threshold + 1, workspaceFree: "unknown", cleanup: true },
|
||||
{ name: "failed df command", dockerFree: threshold + 1, workspaceFree: threshold + 1, dfStatus: "1", cleanup: true },
|
||||
{ name: "failed Docker inspection", dockerFree: threshold + 1, workspaceFree: threshold + 1, infoStatus: "1", cleanup: true },
|
||||
]) {
|
||||
test(`cloud disk cleanup: ${name}`, () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "cloud-disk-test-"));
|
||||
const log = path.join(dir, "commands.log");
|
||||
// Every mutating command is a recording fixture; no real SDKs, caches,
|
||||
// images, or directories are deleted when the workflow shell executes.
|
||||
const fixture = `#!/bin/bash
|
||||
printf '%s %s\\n' "\${0##*/}" "$*" >> "$COMMAND_LOG"
|
||||
case "\${0##*/}" in
|
||||
df)
|
||||
printf 'Filesystem 1024-blocks Used Available Capacity Mounted on\\n'
|
||||
if [ "$1" = '-Pk' ]; then
|
||||
printf '/dev/docker 200000000 1 %s 1%% /docker\\n' "$DOCKER_FREE"
|
||||
printf '/dev/workspace 200000000 1 %s 1%% /workspace\\n' "$WORKSPACE_FREE"
|
||||
exit "$DF_STATUS"
|
||||
fi
|
||||
;;
|
||||
docker)
|
||||
if [ "$1" = 'info' ]; then
|
||||
printf '/docker-data\\n'
|
||||
exit "$INFO_STATUS"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
`;
|
||||
try {
|
||||
for (const command of ["df", "docker", "pnpm", "sudo"]) {
|
||||
writeFileSync(path.join(dir, command), fixture, { mode: 0o755 });
|
||||
}
|
||||
const result = spawnSync("bash", ["-c", script], {
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, PATH: `${dir}:${process.env.PATH}`, GITHUB_WORKSPACE: "/workspace", COMMAND_LOG: log,
|
||||
DOCKER_FREE: String(dockerFree), WORKSPACE_FREE: String(workspaceFree), DF_STATUS: dfStatus, INFO_STATUS: infoStatus },
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
const commands = readFileSync(log, "utf8");
|
||||
if (infoStatus === "0") assert.match(commands, /df -Pk \/docker-data \/workspace/);
|
||||
assert.equal(commands.includes("pnpm store prune"), cleanup);
|
||||
assert.equal(commands.includes("sudo rm -rf /usr/share/dotnet"), cleanup);
|
||||
assert.equal(commands.includes("docker system prune -af"), cleanup);
|
||||
assert.equal(result.stdout.includes("skipping cleanup"), !cleanup);
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ const workflows = [
|
|||
'.github/workflows/refresh-lockfile.yml',
|
||||
'.github/workflows/pr-trusted.yml',
|
||||
'.github/workflows/docker.yml',
|
||||
'.github/workflows/docker-cloud.yml',
|
||||
];
|
||||
|
||||
test('lockfile repair workflows resolve dependencies instead of updating metadata only', async () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const workflow = readFileSync(new URL("../../workflows/release-verify.yml", import.meta.url), "utf8");
|
||||
const runner = workflow.split(" verify_paperclip_runner:")[1].split(" build:")[0];
|
||||
|
||||
test("Runner dependency caching selects the package's pinned compiler before computing its key", () => {
|
||||
const select = runner.indexOf(" - name: Select the pinned Runner Rust toolchain");
|
||||
const cache = runner.indexOf(" - name: Cache Runner Rust dependencies");
|
||||
assert.ok(select >= 0 && cache > select);
|
||||
const setup = runner.slice(select, cache);
|
||||
assert.match(setup, /working-directory: packages\/paperclip-runner/);
|
||||
assert.match(setup, /rustup show active-toolchain/);
|
||||
assert.match(setup, /echo "RUSTUP_TOOLCHAIN=\$toolchain" >> "\$GITHUB_ENV"/);
|
||||
assert.match(runner, /uses: Swatinem\/rust-cache@[0-9a-f]{40} # v[0-9.]+/);
|
||||
assert.match(runner, /workspaces: packages\/paperclip-runner\/runner -> target/);
|
||||
assert.match(runner, /shared-key: release-runner-v1/);
|
||||
});
|
||||
|
||||
test("the shared cache excludes workspace artifacts and only restores or saves the exact master-push source", () => {
|
||||
assert.match(runner, /cache-workspace-crates: false/);
|
||||
assert.match(runner, /cache-bin: false/);
|
||||
const saveIf = runner.match(/^\s*save-if: (.+)$/m)?.[1];
|
||||
assert.equal(saveIf, "${{ github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}");
|
||||
const cacheStep = runner.split(" - name: Cache Runner Rust dependencies")[1].split(" - name: Install dependencies")[0];
|
||||
assert.equal(cacheStep.match(/^\s*if: (.+)$/m)?.[1], saveIf);
|
||||
assert.doesNotMatch(runner, /cache-on-failure: true|cache-all-crates: true/);
|
||||
});
|
||||
|
||||
test("cache hits cannot bypass Runner verification", () => {
|
||||
const verify = runner.split(" - name: Verify Paperclip Runner")[1];
|
||||
assert.match(verify, /run: pnpm --filter @paperclipai\/paperclip-runner check:all/);
|
||||
assert.doesNotMatch(verify, /if:|continue-on-error:/);
|
||||
assert.ok(runner.indexOf("Cache Runner Rust dependencies") < runner.indexOf(" - name: Verify Paperclip Runner\n"));
|
||||
assert.doesNotMatch(runner, /id-token: write|packages: write|secrets: inherit/);
|
||||
});
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
const { branchIndex } = require('./storybook-destination.cjs');
|
||||
|
||||
async function verifyStorybook({ branchUrl, buildUrl, sha, fetch = globalThis.fetch,
|
||||
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), attempts = 6 }) {
|
||||
for (let attempt = 1; attempt <= attempts; attempt++) {
|
||||
try {
|
||||
const [metadata, index] = await Promise.all([
|
||||
fetch(new URL('deployment.json', buildUrl), { signal: AbortSignal.timeout(15000) }),
|
||||
fetch(branchUrl, { signal: AbortSignal.timeout(15000) }),
|
||||
]);
|
||||
if (!metadata.ok || !index.ok) throw new Error(`Public deployment returned HTTP ${metadata.status}/${index.status}.`);
|
||||
if ((await metadata.json()).sha !== sha) throw new Error('Public build has the wrong source commit.');
|
||||
if ((await index.text()) !== branchIndex(buildUrl)) throw new Error('Public branch URL does not point to this build.');
|
||||
return;
|
||||
} catch (error) {
|
||||
if (attempt === attempts) throw error;
|
||||
await sleep(10000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { verifyStorybook };
|
||||
if (require.main === module) {
|
||||
verifyStorybook({ branchUrl: process.env.BRANCH_URL, buildUrl: process.env.BUILD_URL,
|
||||
sha: process.env.SOURCE_SHA }).catch((error) => { console.error(error); process.exitCode = 1; });
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"Sid": "AllowCloudFrontReadStorybook",
|
||||
"Effect": "Allow",
|
||||
"Principal": {"Service": "cloudfront.amazonaws.com"},
|
||||
"Action": "s3:GetObject",
|
||||
"Resource": "arn:aws:s3:::paperclipai-runner-e2e-history-078455283791-us-east-1/storybook/branches/*",
|
||||
"Condition": {"StringEquals": {
|
||||
"AWS:SourceArn": "arn:aws:cloudfront::078455283791:distribution/E3GTU28BBO2SFR"
|
||||
}}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Principal": {"Federated": "arn:aws:iam::078455283791:oidc-provider/token.actions.githubusercontent.com"},
|
||||
"Action": "sts:AssumeRoleWithWebIdentity",
|
||||
"Condition": {"StringEquals": {
|
||||
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
|
||||
"token.actions.githubusercontent.com:sub": "repo:paperclipai/paperclip:environment:storybook-deploy"
|
||||
}}
|
||||
}]
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Action": ["s3:PutObject", "s3:GetObject", "s3:AbortMultipartUpload"],
|
||||
"Resource": "arn:aws:s3:::paperclipai-runner-e2e-history-078455283791-us-east-1/storybook/branches/*"
|
||||
}]
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
name: Cloud artifacts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
dispatch_migrator:
|
||||
name: Start exact-source cloud migrator publication
|
||||
if: github.repository == 'paperclipai/paperclip' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
actions: write
|
||||
steps:
|
||||
# This separate workflow starts at merge, outside the full npm release's
|
||||
# concurrency group. Publication stays in release.yml so npm recognizes
|
||||
# the established trusted-publisher identity and npm-canary environment.
|
||||
# No source checkout or package code runs with the dispatch credential.
|
||||
- name: Dispatch the migrator-only release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
request_id="$(cat /proc/sys/kernel/random/uuid)"
|
||||
gh workflow run release.yml --repo "$GITHUB_REPOSITORY" --ref master \
|
||||
--field channel=cloud-migrator \
|
||||
--field source_ref="$SOURCE_SHA" \
|
||||
--field request_id="$request_id"
|
||||
echo "Started Cloud migrator $SOURCE_SHA in release.yml (request $request_id)." >> "$GITHUB_STEP_SUMMARY"
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
name: Cloud readiness
|
||||
run-name: Cloud readiness ${{ github.sha }}
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
# Source verification must start outside the full npm release's queue.
|
||||
concurrency:
|
||||
group: cloud-readiness-${{ github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
image:
|
||||
if: github.repository == 'paperclipai/paperclip' && github.ref == 'refs/heads/master'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
uses: ./.github/workflows/docker-cloud.yml
|
||||
|
||||
verify:
|
||||
if: github.repository == 'paperclipai/paperclip' && github.ref == 'refs/heads/master'
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/release-verify.yml
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
|
||||
artifacts:
|
||||
if: github.repository == 'paperclipai/paperclip' && github.ref == 'refs/heads/master'
|
||||
name: Wait for exact-source cloud artifacts
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 35
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Wait for verified image and exact-source migrator
|
||||
env:
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
run: node scripts/cloud-readiness.mjs "$SOURCE_SHA"
|
||||
|
||||
source_verified:
|
||||
# npm canary publication reuses this exact-source verification proof.
|
||||
# Keep it independent of image/migrator availability, and fail closed when
|
||||
# any source check fails, is cancelled, or is skipped.
|
||||
name: Cloud source verified v1
|
||||
needs: [verify]
|
||||
if: github.repository == 'paperclipai/paperclip' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Check the source verification consumer
|
||||
run: node --test scripts/cloud-source-verification.test.mjs
|
||||
- name: Record source verification
|
||||
env:
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
echo "Cloud source verified v1: $SOURCE_SHA" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
ready:
|
||||
# Versioned consumer contract. Never add always() or continue-on-error:
|
||||
# failed, cancelled, or skipped prerequisites must not report readiness.
|
||||
name: Cloud deployable v1
|
||||
needs: [verify, image, artifacts]
|
||||
if: github.repository == 'paperclipai/paperclip' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Record cloud readiness
|
||||
env:
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
echo "Cloud deployable v1: $SOURCE_SHA" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Source verification passed; the full-SHA image and exact-source migrator are available." >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Deployment tooling must still resolve and pin the image and migrator and validate migration compatibility." >> "$GITHUB_STEP_SUMMARY"
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
name: Docker cloud
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
permissions: {}
|
||||
|
||||
# Independent SHAs can build immediately on separate runners.
|
||||
# Repeated requests for the same source serialize without cancelling a build.
|
||||
# No mutable canary channel is promoted here; docker.yml owns that operation.
|
||||
concurrency:
|
||||
group: docker-cloud-${{ github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build-and-push-cloud:
|
||||
# Only canonical master builds can consume the release Fleet. The runner
|
||||
# group must also allow this workflow only at refs/heads/master.
|
||||
# Keep an operator switch for a full-run retry on GitHub-hosted runners.
|
||||
runs-on: ${{ vars.AWS_CLOUD_BUILDS_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && 'runs-on/fleet=paperclip-cloud-build-x64/env=public-ci' || 'ubuntu-latest' }}
|
||||
# Fleet instances expire after 45 minutes, including bootstrap and cleanup.
|
||||
timeout-minutes: ${{ vars.AWS_CLOUD_BUILDS_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && 40 || 60 }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
# Full history and tags so `git describe` below can compute the
|
||||
# release version to stamp into the image.
|
||||
fetch-depth: 0
|
||||
|
||||
# `.git` is dockerignored, so a running image cannot derive its own
|
||||
# version and otherwise reports the source package.json placeholder in
|
||||
# analytics and the debug panel. Compute it here from the pristine
|
||||
# checkout (real CalVer drift from the nearest release tag) and pass it
|
||||
# into the build. Empty when no release tag is reachable — the server
|
||||
# then keeps its existing fallbacks.
|
||||
- name: Compute build version
|
||||
id: build-version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "${GITHUB_REF}" in
|
||||
refs/tags/nightly/v*)
|
||||
# Lane tags carry the exact published version; stamp it verbatim
|
||||
# instead of describing drift from the nearest stable tag.
|
||||
version="${GITHUB_REF#refs/tags/nightly/v}"
|
||||
;;
|
||||
refs/tags/beta/v*)
|
||||
version="${GITHUB_REF#refs/tags/beta/v}"
|
||||
;;
|
||||
*)
|
||||
version="$(git describe --tags --match 'v*' --long --dirty 2>/dev/null || true)"
|
||||
;;
|
||||
esac
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
echo "Stamping build version: ${version:-<none>}"
|
||||
|
||||
# ISO week stamp for the Dockerfile's tool layer: the layer caches
|
||||
# across commits and re-pulls the @latest CLI tools when the week rolls
|
||||
# over, instead of on every build.
|
||||
- name: Compute tool cache epoch
|
||||
id: tools-epoch
|
||||
run: echo "epoch=$(date -u +%G-W%V)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Each SHA exports its own cache. Import recent first-parent caches so
|
||||
# a late older build cannot overwrite a newer build's cache manifest.
|
||||
# The legacy ref keeps the first builds warm during the transition.
|
||||
- name: Select cloud cache ancestry
|
||||
id: cloud-cache
|
||||
env:
|
||||
CACHE_IMAGE: ghcr.io/${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
{
|
||||
echo 'sources<<CACHE_SOURCES'
|
||||
for commit in $(git rev-list --first-parent --max-count=10 HEAD); do
|
||||
echo "type=registry,ref=$CACHE_IMAGE:buildcache-cloud-$commit"
|
||||
done
|
||||
echo "type=registry,ref=$CACHE_IMAGE:buildcache-cloud"
|
||||
echo 'CACHE_SOURCES'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
||||
with:
|
||||
version: 9.15.4
|
||||
run_install: false
|
||||
|
||||
# No dependency cache here: this workflow publishes release images, and
|
||||
# restoring a shared Actions cache into the build inputs would let a
|
||||
# poisoned cache entry reach the published artifact.
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Refresh lockfile for Docker build context
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile
|
||||
|
||||
changed="$(git status --porcelain)"
|
||||
if [ -z "$changed" ]; then
|
||||
echo "Lockfile already matches package metadata."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if printf '%s\n' "$changed" | grep -Fvq ' pnpm-lock.yaml'; then
|
||||
echo "Unexpected files changed during lockfile refresh:"
|
||||
echo "$changed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Using refreshed pnpm-lock.yaml in the Docker build context."
|
||||
|
||||
- name: Free runner disk
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Disk before cleanup:"
|
||||
df -h
|
||||
|
||||
# A measured hosted cloud build started with 86 GB available.
|
||||
# Keep ample headroom for BuildKit and image verification, but
|
||||
# avoid minutes deleting SDKs when neither filesystem needs space.
|
||||
minimum_free_kib=$((64 * 1024 * 1024))
|
||||
if docker_root="$(docker info --format '{{.DockerRootDir}}')" \
|
||||
&& available_kib="$(df -Pk "$docker_root" "$GITHUB_WORKSPACE" | awk 'NR > 1 { rows++; if ($4 !~ /^[0-9]+$/) invalid = 1; if (min == "" || $4 < min) min = $4 } END { if (invalid || rows != 2) exit 1; print min }')" \
|
||||
&& [[ "$available_kib" =~ ^[0-9]+$ ]] \
|
||||
&& (( available_kib >= minimum_free_kib )); then
|
||||
echo "At least 64 GiB is available for Docker and the workspace; skipping cleanup."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
pnpm store prune || true
|
||||
sudo apt-get clean || true
|
||||
sudo rm -rf \
|
||||
/usr/share/dotnet \
|
||||
/usr/share/swift \
|
||||
/usr/local/lib/android \
|
||||
/usr/local/share/boost \
|
||||
/usr/local/share/powershell \
|
||||
/opt/ghc \
|
||||
/opt/hostedtoolcache/CodeQL \
|
||||
/opt/hostedtoolcache/PyPy \
|
||||
/opt/hostedtoolcache/Ruby || true
|
||||
docker system prune -af || true
|
||||
|
||||
echo "Disk after cleanup:"
|
||||
df -h
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
|
||||
# Deployment tooling reads these labels from the registry to verify an
|
||||
# image's schema expectations against a migrator before deploying it,
|
||||
# without pulling the image. The server refuses to start when the
|
||||
# database is missing bundled migrations, so orchestrators need a cheap
|
||||
# way to check image/migrator compatibility up front.
|
||||
- name: Compute schema migration labels
|
||||
id: schema
|
||||
run: |
|
||||
set -euo pipefail
|
||||
last=$(ls packages/db/src/migrations/*.sql | sed 's|.*/||' | LC_ALL=C sort | tail -1)
|
||||
count=$(ls packages/db/src/migrations/*.sql | wc -l | tr -d ' ')
|
||||
echo "last=${last}" >> "$GITHUB_OUTPUT"
|
||||
echo "count=${count}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Published under the same lane tag set as the self-hosted image, with a
|
||||
# `-cloud` suffix (nightly-cloud, latest-cloud, <version>-cloud,
|
||||
# sha-<short>-cloud). `:canary-cloud` follows the same retag-step
|
||||
# ownership rule as `:canary` above.
|
||||
- name: Docker meta (cloud)
|
||||
id: meta-cloud
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
flavor: |
|
||||
suffix=-cloud,onlatest=true
|
||||
tags: |
|
||||
type=raw,value=nightly,enable=${{ startsWith(github.ref, 'refs/tags/nightly/v') }}
|
||||
type=raw,value=beta,enable=${{ startsWith(github.ref, 'refs/tags/beta/v') }}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=sha
|
||||
labels: |
|
||||
io.github.paperclipai.schema.last-migration=${{ steps.schema.outputs.last }}
|
||||
io.github.paperclipai.schema.migration-count=${{ steps.schema.outputs.count }}
|
||||
|
||||
- name: Build and push (cloud)
|
||||
id: build-cloud
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
with:
|
||||
context: .
|
||||
target: cloud
|
||||
# Space-separated sandbox-provider directory names to build into
|
||||
# the variant; add here when managed deployments need another.
|
||||
# CLOUD_BUNDLED_SERVER_DEPS names the optional peer packages the
|
||||
# variant installs from server/package.json's declared version;
|
||||
# add another name there when a managed tenant needs it.
|
||||
build-args: |
|
||||
USER_UID=1001
|
||||
USER_GID=1001
|
||||
CLOUD_BUNDLED_PLUGINS=daytona
|
||||
CLOUD_BUNDLED_SERVER_DEPS=@sentry/node
|
||||
PAPERCLIP_BUILD_VERSION=${{ steps.build-version.outputs.version }}
|
||||
PAPERCLIP_BUILD_COMMIT=${{ github.sha }}
|
||||
CLI_TOOLS_CACHE_EPOCH=${{ steps.tools-epoch.outputs.epoch }}
|
||||
# amd64 only, unlike the self-hosted image above: the cloud variant
|
||||
# is consumed exclusively by managed-deployment hosts, which run
|
||||
# amd64. The QEMU-emulated arm64 half dominated this job's wall
|
||||
# clock, and dropping it roughly halves time-to-deployable-image.
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
# Same-SHA builds serialize above; different SHAs never share a
|
||||
# writable cache ref. Registry layers are content-addressed and
|
||||
# shared even when cache manifests have separate tags.
|
||||
cache-from: ${{ steps.cloud-cache.outputs.sources }}
|
||||
cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache-cloud-${{ github.sha }},mode=max
|
||||
tags: ${{ steps.meta-cloud.outputs.tags }}
|
||||
labels: ${{ steps.meta-cloud.outputs.labels }}
|
||||
|
||||
# The cloud target installs @sentry/node at the version
|
||||
# server/package.json declares, into a directory the server's own
|
||||
# module resolution walks. Verify the image this job just pushed, not
|
||||
# a local build, so a build-cache or layer-ordering regression is
|
||||
# caught before any tenant runs the image.
|
||||
|
||||
- name: Verify the pushed image resolves the declared Sentry version
|
||||
env:
|
||||
IMAGE: ghcr.io/${{ github.repository }}@${{ steps.build-cloud.outputs.digest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
expected="$(node -e "process.stdout.write(require('./server/package.json').peerDependencies['@sentry/node'])")"
|
||||
test -n "$expected"
|
||||
|
||||
installed="$(docker run --rm --pull always \
|
||||
-v "$PWD/scripts/assert-cloud-image-sentry.mjs:/app/server/.ci-sentry-probe.mjs:ro" \
|
||||
--entrypoint node "$IMAGE" /app/server/.ci-sentry-probe.mjs)"
|
||||
|
||||
echo "Declared optional peer version: $expected"
|
||||
echo "Installed in the pushed image: $installed"
|
||||
if [ "$installed" != "$expected" ]; then
|
||||
echo "ERROR: the pushed image resolves @sentry/node@$installed, expected @sentry/node@$expected" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "The pushed image resolves the declared @sentry/node version."
|
||||
|
||||
# Managed hosts run node as 1001:1001. Bake that identity into the image
|
||||
# so usermod does not walk the mounted home on every container start.
|
||||
# Check before the entrypoint can repair a wrongly built identity.
|
||||
- name: Verify cloud runtime user
|
||||
env:
|
||||
IMAGE: ghcr.io/${{ github.repository }}@${{ steps.build-cloud.outputs.digest }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker run --rm --entrypoint sh "$IMAGE" -ec '
|
||||
test "$(id -u node)" = 1001
|
||||
test "$(id -g node)" = 1001
|
||||
test "$USER_UID" = 1001
|
||||
test "$USER_GID" = 1001
|
||||
'
|
||||
docker run --rm -e USER_UID=1001 -e USER_GID=1001 "$IMAGE" sh -ec '
|
||||
test "$(id -u)" = 1001
|
||||
test "$(id -g)" = 1001
|
||||
test -w "$PAPERCLIP_HOME"
|
||||
'
|
||||
|
||||
# Verify the independently published cloud image without waiting for
|
||||
# the self-hosted manifest job. The Sentry check already pulled it.
|
||||
- name: Verify cloud PID 1 reaps orphaned processes
|
||||
env:
|
||||
IMAGE: ghcr.io/${{ github.repository }}@${{ steps.build-cloud.outputs.digest }}
|
||||
run: docker run --rm -i "$IMAGE" sh -s < scripts/assert-orphan-reaping.sh
|
||||
|
||||
# Cloud's commit resolver and preview-artifact planner use the full SHA.
|
||||
# Publish that address only after checking this build's exact digest.
|
||||
# Retagging reuses the registry manifest and does not rebuild the image.
|
||||
- name: Publish verified full-SHA cloud tag
|
||||
env:
|
||||
IMAGE: ghcr.io/${{ github.repository }}@${{ steps.build-cloud.outputs.digest }}
|
||||
FULL_SHA_TAG: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}-cloud
|
||||
run: |
|
||||
set -euo pipefail
|
||||
revision="$(docker image inspect "$IMAGE" --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}')"
|
||||
platform="$(docker image inspect "$IMAGE" --format '{{ .Os }}/{{ .Architecture }}')"
|
||||
test "$revision" = "$GITHUB_SHA"
|
||||
test "$platform" = linux/amd64
|
||||
docker buildx imagetools create --prefer-index=false --tag "$FULL_SHA_TAG" "$IMAGE"
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
name: Docker Runner check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/docker-runner-check.yml
|
||||
- Dockerfile
|
||||
- .dockerignore
|
||||
- packages/paperclip-runner/rust-toolchain.toml
|
||||
- packages/paperclip-runner/runner/**
|
||||
- packages/paperclip-runner/protocol/**
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: docker-runner-check-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
runner:
|
||||
name: Compile isolated native Runner
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
# Compile the real target with the real .dockerignore. This catches new
|
||||
# Cargo or embedded protocol inputs that the isolated COPY set omits.
|
||||
# No registry credentials, cache imports/exports, or image publication.
|
||||
- name: Compile the Runner from its isolated Docker context
|
||||
run: docker buildx build --target runner-build --progress plain .
|
||||
|
|
@ -24,13 +24,20 @@ on:
|
|||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
# Least privilege: nothing at the workflow level; each job declares exactly
|
||||
# the token scopes it uses (checkout needs contents:read, GHCR pushes need
|
||||
# packages:write).
|
||||
permissions: {}
|
||||
|
||||
# Serialise builds per ref without killing an in-flight one: a newer push
|
||||
# supersedes only the pending slot, so the image build that is already
|
||||
# running always finishes and publishes.
|
||||
# running always finishes and publishes. Canary TAG refs each get their
|
||||
# own group on purpose: their builds run in parallel so every published
|
||||
# canary gets its sha images regardless of merge cadence. The mutable
|
||||
# `:canary` channel tags are NOT written by the build matrix (which
|
||||
# would race across parallel runs) — each canary-tag run retags the
|
||||
# channel afterwards, only if it still matches the npm `canary`
|
||||
# dist-tag, so the channel moves monotonically and always mirrors npm.
|
||||
concurrency:
|
||||
group: docker-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
|
@ -77,9 +84,51 @@ jobs:
|
|||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
# Multi-arch by native runner, not QEMU.
|
||||
#
|
||||
# This was one job building linux/amd64,linux/arm64 together on an x86
|
||||
# runner. The arm64 half is emulated there, and it did not merely run slow:
|
||||
# it wedged, every time, in `RUN pnpm --filter @paperclipai/server build`,
|
||||
# emitting nothing for 38-45 minutes until `timeout-minutes: 60` killed the
|
||||
# job. Verified across three consecutive runs on 2026-09-04; the amd64 half
|
||||
# reached `production 5/5` minutes earlier in every one.
|
||||
#
|
||||
# A timed-out job is reported as *cancelled*, not failed, so the run read
|
||||
# "cancelled" and the production image simply stopped publishing without
|
||||
# anything going red in an obvious way.
|
||||
#
|
||||
# It also starved the queue. A run that burns the full hour holds the
|
||||
# top-level concurrency slot for that hour, and `cancel-in-progress: false`
|
||||
# keeps exactly one pending slot — so with merges arriving faster than one
|
||||
# an hour, most runs were superseded before they ever started a job. Five of
|
||||
# ten master commits sampled that day never produced an image at all.
|
||||
#
|
||||
# Each platform now builds on a runner of its own architecture and pushes by
|
||||
# digest; `merge` assembles the manifest list. arm64 is kept rather than
|
||||
# dropped (the cloud variant below dropped it and is amd64-only) because
|
||||
# this is the self-hosted image, and ARM hosts consume it.
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
# Independent legs: one architecture failing should still publish
|
||||
# nothing, but it must not also hide the other's logs behind a
|
||||
# cancellation.
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
arch: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
# Native builds land well inside this; it is a backstop, not a budget.
|
||||
# (The interim fix while this PR landed raised the single QEMU job's cap
|
||||
# to 120 minutes; native per-arch legs make that headroom unnecessary.)
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
|
@ -209,16 +258,18 @@ jobs:
|
|||
echo "last=${last}" >> "$GITHUB_OUTPUT"
|
||||
echo "count=${count}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Lane tag mapping: master pushes publish `:canary`, nightly/v* tags
|
||||
# publish `:nightly`, and only stable v* tags move `:latest` and the
|
||||
# versioned tags. `:sha-<short>` is published on every build.
|
||||
# Lane tag mapping: nightly/v* tags publish `:nightly`, and only
|
||||
# stable v* tags move `:latest` and the versioned tags.
|
||||
# `:sha-<short>` is published on every build. `:canary` is
|
||||
# deliberately absent here — the channel tag is moved by the
|
||||
# dist-tag-checked retag step below, never by the build matrix,
|
||||
# so parallel canary builds cannot race it backwards.
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=raw,value=canary,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=nightly,enable=${{ startsWith(github.ref, 'refs/tags/nightly/v') }}
|
||||
type=raw,value=beta,enable=${{ startsWith(github.ref, 'refs/tags/beta/v') }}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
|
@ -228,8 +279,8 @@ jobs:
|
|||
labels: |
|
||||
io.github.paperclipai.schema.last-migration=${{ steps.schema.outputs.last }}
|
||||
io.github.paperclipai.schema.migration-count=${{ steps.schema.outputs.count }}
|
||||
|
||||
- name: Build and push
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
|
|
@ -241,25 +292,123 @@ jobs:
|
|||
PAPERCLIP_BUILD_VERSION=${{ steps.build-version.outputs.version }}
|
||||
PAPERCLIP_BUILD_COMMIT=${{ github.sha }}
|
||||
CLI_TOOLS_CACHE_EPOCH=${{ steps.tools-epoch.outputs.epoch }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
platforms: ${{ matrix.platform }}
|
||||
# By digest, not by tag: two runners cannot each push the same tag
|
||||
# and end up with a manifest list. Each leg publishes an untagged
|
||||
# image and `merge` names them together.
|
||||
outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true
|
||||
# Registry-backed BuildKit cache instead of type=gha: the Actions
|
||||
# cache is capped at 10GB per repo, and two multi-arch mode=max jobs
|
||||
# evict each other, so most builds ran effectively cold. The cache
|
||||
# ref lives in ghcr next to the image and is written only by this
|
||||
# workflow (docker.yml runs on master/tag pushes, never on PRs).
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache
|
||||
cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
#
|
||||
# Per-arch refs now the legs are separate runners: a shared ref would
|
||||
# have each leg overwrite the other's cache on every build.
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache-${{ matrix.arch }}
|
||||
cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache-${{ matrix.arch }},mode=max
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
# The digest is the only thing `merge` needs from this job. Carried as an
|
||||
# empty file named for it, which is the upstream pattern — the name is
|
||||
# the payload, so several legs can upload without colliding on content.
|
||||
- name: Export digest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
test -n "$digest"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: digests-production-${{ matrix.arch }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
# Names the per-architecture digests as one manifest list under the real
|
||||
# tags. Nothing is publicly tagged until this runs, so a half-published
|
||||
# multi-arch image is not a state anything can pull.
|
||||
merge-and-push:
|
||||
needs: build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
# Checked out for `packages/db` (schema labels) and the orphan-reaping
|
||||
# script the verification step pipes in.
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-production-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
|
||||
# Repeated from the build job rather than passed between them: job
|
||||
# outputs would have to survive a matrix, and this is two `ls` calls.
|
||||
- name: Compute schema migration labels
|
||||
id: schema
|
||||
run: |
|
||||
set -euo pipefail
|
||||
last=$(ls packages/db/src/migrations/*.sql | sed 's|.*/||' | LC_ALL=C sort | tail -1)
|
||||
count=$(ls packages/db/src/migrations/*.sql | wc -l | tr -d ' ')
|
||||
echo "last=${last}" >> "$GITHUB_OUTPUT"
|
||||
echo "count=${count}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Same lane mapping as the build job; this is where it is actually
|
||||
# applied, since the legs push untagged. `:canary` is deliberately
|
||||
# absent, exactly as in the build job's mapping: the channel tag is
|
||||
# moved only by the dist-tag-checked promote_canary_channel job below,
|
||||
# so parallel canary-tag builds can never race the channel backwards.
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=raw,value=nightly,enable=${{ startsWith(github.ref, 'refs/tags/nightly/v') }}
|
||||
type=raw,value=beta,enable=${{ startsWith(github.ref, 'refs/tags/beta/v') }}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=sha
|
||||
labels: |
|
||||
io.github.paperclipai.schema.last-migration=${{ steps.schema.outputs.last }}
|
||||
io.github.paperclipai.schema.migration-count=${{ steps.schema.outputs.count }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker buildx imagetools create \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *)
|
||||
|
||||
# PID 1 must be an init that reaps adopted orphans. With node there, the
|
||||
# orphans agent runs leave behind are never wait()ed and pin as zombies
|
||||
# until the cgroup pid limit is exhausted and every fork() in the
|
||||
# container fails. Run against the pushed image rather than a local
|
||||
# build: the step above is multi-arch with `push: true`, so nothing is
|
||||
# loaded into the runner's daemon. The cloud variant is FROM production
|
||||
# and inherits the same ENTRYPOINT, so checking this image covers both.
|
||||
# container fails. Run against the pushed manifest rather than a local
|
||||
# build: the legs push by digest, so nothing is loaded into this
|
||||
# runner's daemon. The independent cloud workflow checks its own image.
|
||||
- name: Verify PID 1 reaps orphaned processes
|
||||
env:
|
||||
# Through the environment, not interpolated into the script body, so
|
||||
|
|
@ -272,121 +421,47 @@ jobs:
|
|||
echo "Verifying orphan reaping in $image"
|
||||
docker run --rm -i --pull always "$image" sh -s < scripts/assert-orphan-reaping.sh
|
||||
|
||||
# The cloud variant carries built bundled plugins for managed deployments
|
||||
# (see the `cloud` stage in the Dockerfile). It runs as its own job with no
|
||||
# `needs:` on the stock publish above, so the two builds run in parallel and
|
||||
# a failure or slow build in one never gates, delays, or skips the other.
|
||||
# Both jobs share only the single top-level concurrency slot. Each job is a
|
||||
# separate runner, so this one carries its own copy of the prep steps
|
||||
# (checkout through schema labels) — the accepted cost of that isolation.
|
||||
# Master cloud builds start independently in docker-cloud.yml. Tag builds
|
||||
# and manual Docker dispatches call the same implementation, preserving the
|
||||
# release tags and the canary promotion dependency below.
|
||||
build-and-push-cloud:
|
||||
if: github.event_name != 'push' || github.ref != 'refs/heads/master'
|
||||
uses: ./.github/workflows/docker-cloud.yml
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
# Moves the mutable `:canary` / `:canary-cloud` channel tags. Kept OUT
|
||||
# of the build jobs and serialized in its own lane, and — the load-
|
||||
# bearing property — CONVERGENT rather than self-interested: a
|
||||
# promotion does not promote "its own" canary, it retags the channel
|
||||
# to whatever the npm `canary` dist-tag names at execution time,
|
||||
# provided that version's sha images are published. GitHub's shared
|
||||
# concurrency lane keeps one running and one pending promotion and
|
||||
# REPLACES the pending slot with the latest enqueued — an older build
|
||||
# finishing late can therefore evict the newest canary's pending
|
||||
# promotion. With convergent promotion that eviction is harmless:
|
||||
# whichever promotion survives resolves the current dist-tag fresh
|
||||
# and lands the channel there (the current canary's images always
|
||||
# exist by the time any later promotion runs, because per-tag build
|
||||
# groups mean canary builds are never superseded and each run's
|
||||
# promotion is gated on its own completed pushes). Every interleaving
|
||||
# converges the Docker channel onto the npm channel.
|
||||
promote_canary_channel:
|
||||
if: startsWith(github.ref, 'refs/tags/canary/v')
|
||||
# merge-and-push, not build-and-push: the per-arch legs push untagged
|
||||
# digests, and the production `sha-*` tags this promotion retags only
|
||||
# exist once the manifest merge has named them.
|
||||
needs: [merge-and-push, build-and-push-cloud]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
concurrency:
|
||||
group: docker-canary-channel-promotion
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
# Full history and tags so `git describe` below can compute the
|
||||
# release version to stamp into the image.
|
||||
fetch-depth: 0
|
||||
|
||||
# `.git` is dockerignored, so a running image cannot derive its own
|
||||
# version and otherwise reports the source package.json placeholder in
|
||||
# analytics and the debug panel. Compute it here from the pristine
|
||||
# checkout (real CalVer drift from the nearest release tag) and pass it
|
||||
# into the build. Empty when no release tag is reachable — the server
|
||||
# then keeps its existing fallbacks.
|
||||
- name: Compute build version
|
||||
id: build-version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "${GITHUB_REF}" in
|
||||
refs/tags/nightly/v*)
|
||||
# Lane tags carry the exact published version; stamp it verbatim
|
||||
# instead of describing drift from the nearest stable tag.
|
||||
version="${GITHUB_REF#refs/tags/nightly/v}"
|
||||
;;
|
||||
refs/tags/beta/v*)
|
||||
version="${GITHUB_REF#refs/tags/beta/v}"
|
||||
;;
|
||||
*)
|
||||
version="$(git describe --tags --match 'v*' --long --dirty 2>/dev/null || true)"
|
||||
;;
|
||||
esac
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
echo "Stamping build version: ${version:-<none>}"
|
||||
|
||||
# ISO week stamp for the Dockerfile's tool layer: the layer caches
|
||||
# across commits and re-pulls the @latest CLI tools when the week rolls
|
||||
# over, instead of on every build.
|
||||
- name: Compute tool cache epoch
|
||||
id: tools-epoch
|
||||
run: echo "epoch=$(date -u +%G-W%V)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 9.15.4
|
||||
run_install: false
|
||||
|
||||
# No dependency cache here: this workflow publishes release images, and
|
||||
# restoring a shared Actions cache into the build inputs would let a
|
||||
# poisoned cache entry reach the published artifact.
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Refresh lockfile for Docker build context
|
||||
env:
|
||||
STAGING_ARTIFACT_BASE_URL: ${{ inputs.staging_artifact_base_url }}
|
||||
EXPECTED_LOCK_SHA256: ${{ inputs.staging_lock_sha256 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile
|
||||
|
||||
if [ -n "$STAGING_ARTIFACT_BASE_URL" ]; then
|
||||
[[ "$EXPECTED_LOCK_SHA256" =~ ^[a-f0-9]{64}$ ]]
|
||||
echo "$EXPECTED_LOCK_SHA256 pnpm-lock.yaml" | sha256sum --check --strict
|
||||
fi
|
||||
|
||||
changed="$(git status --porcelain)"
|
||||
if [ -z "$changed" ]; then
|
||||
echo "Lockfile already matches package metadata."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if printf '%s\n' "$changed" | grep -Fvq ' pnpm-lock.yaml'; then
|
||||
echo "Unexpected files changed during lockfile refresh:"
|
||||
echo "$changed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Using refreshed pnpm-lock.yaml in the Docker build context."
|
||||
|
||||
- name: Free runner disk
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Disk before cleanup:"
|
||||
df -h
|
||||
|
||||
pnpm store prune || true
|
||||
sudo apt-get clean || true
|
||||
sudo rm -rf \
|
||||
/usr/share/dotnet \
|
||||
/usr/share/swift \
|
||||
/usr/local/lib/android \
|
||||
/usr/local/share/boost \
|
||||
/usr/local/share/powershell \
|
||||
/opt/ghc \
|
||||
/opt/hostedtoolcache/CodeQL \
|
||||
/opt/hostedtoolcache/PyPy \
|
||||
/opt/hostedtoolcache/Ruby || true
|
||||
docker system prune -af || true
|
||||
|
||||
echo "Disk after cleanup:"
|
||||
df -h
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
|
|
@ -394,99 +469,22 @@ jobs:
|
|||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
# Deployment tooling reads these labels from the registry to verify an
|
||||
# image's schema expectations against a migrator before deploying it,
|
||||
# without pulling the image. The server refuses to start when the
|
||||
# database is missing bundled migrations, so orchestrators need a cheap
|
||||
# way to check image/migrator compatibility up front.
|
||||
- name: Compute schema migration labels
|
||||
id: schema
|
||||
run: |
|
||||
set -euo pipefail
|
||||
last=$(ls packages/db/src/migrations/*.sql | sed 's|.*/||' | LC_ALL=C sort | tail -1)
|
||||
count=$(ls packages/db/src/migrations/*.sql | wc -l | tr -d ' ')
|
||||
echo "last=${last}" >> "$GITHUB_OUTPUT"
|
||||
echo "count=${count}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Published under the same lane tag set as the self-hosted image, with a
|
||||
# `-cloud` suffix (canary-cloud, nightly-cloud, latest-cloud,
|
||||
# <version>-cloud, sha-<short>-cloud).
|
||||
- name: Docker meta (cloud)
|
||||
id: meta-cloud
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
flavor: |
|
||||
suffix=-cloud,onlatest=true
|
||||
tags: |
|
||||
type=raw,value=canary,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
type=raw,value=nightly,enable=${{ startsWith(github.ref, 'refs/tags/nightly/v') }}
|
||||
type=raw,value=beta,enable=${{ startsWith(github.ref, 'refs/tags/beta/v') }}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=sha
|
||||
labels: |
|
||||
io.github.paperclipai.schema.last-migration=${{ steps.schema.outputs.last }}
|
||||
io.github.paperclipai.schema.migration-count=${{ steps.schema.outputs.count }}
|
||||
|
||||
- name: Build and push (cloud)
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
target: cloud
|
||||
# Space-separated sandbox-provider directory names to build into
|
||||
# the variant; add here when managed deployments need another.
|
||||
# CLOUD_BUNDLED_SERVER_DEPS names the optional peer packages the
|
||||
# variant installs from server/package.json's declared version;
|
||||
# add another name there when a managed tenant needs it.
|
||||
build-args: |
|
||||
CLOUD_BUNDLED_PLUGINS=daytona
|
||||
CLOUD_BUNDLED_SERVER_DEPS=@sentry/node
|
||||
PAPERCLIP_BUILD_VERSION=${{ steps.build-version.outputs.version }}
|
||||
PAPERCLIP_BUILD_COMMIT=${{ github.sha }}
|
||||
CLI_TOOLS_CACHE_EPOCH=${{ steps.tools-epoch.outputs.epoch }}
|
||||
# amd64 only, unlike the self-hosted image above: the cloud variant
|
||||
# is consumed exclusively by managed-deployment hosts, which run
|
||||
# amd64. The QEMU-emulated arm64 half dominated this job's wall
|
||||
# clock, and dropping it roughly halves time-to-deployable-image.
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
# Registry-backed BuildKit cache, separate ref from the self-hosted
|
||||
# job so the two parallel builds never clobber each other's cache
|
||||
# manifest (see the rationale on the job above).
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache-cloud
|
||||
cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache-cloud,mode=max
|
||||
tags: ${{ steps.meta-cloud.outputs.tags }}
|
||||
labels: ${{ steps.meta-cloud.outputs.labels }}
|
||||
|
||||
# The cloud target installs @sentry/node at the version
|
||||
# server/package.json declares, into a directory the server's own
|
||||
# module resolution walks. Verify the image this job just pushed, not
|
||||
# a local build, so a build-cache or layer-ordering regression is
|
||||
# caught before any tenant runs the image.
|
||||
- name: Verify the pushed image resolves the declared Sentry version
|
||||
- name: Converge the channel tags onto the current npm canary
|
||||
env:
|
||||
IMAGE_TAGS: ${{ steps.meta-cloud.outputs.tags }}
|
||||
IMAGE: ghcr.io/${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
image="$(printf '%s\n' "$IMAGE_TAGS" | head -n 1)"
|
||||
test -n "$image"
|
||||
|
||||
expected="$(node -e "process.stdout.write(require('./server/package.json').peerDependencies['@sentry/node'])")"
|
||||
test -n "$expected"
|
||||
|
||||
installed="$(docker run --rm --pull always \
|
||||
-v "$PWD/scripts/assert-cloud-image-sentry.mjs:/app/server/.ci-sentry-probe.mjs:ro" \
|
||||
--entrypoint node "$image" /app/server/.ci-sentry-probe.mjs)"
|
||||
|
||||
echo "Declared optional peer version: $expected"
|
||||
echo "Installed in the pushed image: $installed"
|
||||
if [ "$installed" != "$expected" ]; then
|
||||
echo "ERROR: the pushed image resolves @sentry/node@$installed, expected @sentry/node@$expected" >&2
|
||||
exit 1
|
||||
current="$(curl -fsS "https://registry.npmjs.org/-/package/@paperclipai%2Fdb/dist-tags" | jq -er .canary)"
|
||||
sha="$(gh api "repos/${GITHUB_REPOSITORY}/commits/$(printf 'canary/v%s' "$current" | jq -sRr @uri)" --jq .sha 2>/dev/null || true)"
|
||||
if [ -z "$sha" ]; then
|
||||
echo "canary/v${current} does not resolve yet; a later promotion converges the channel"
|
||||
exit 0
|
||||
fi
|
||||
echo "The pushed image resolves the declared @sentry/node version."
|
||||
short="$(printf '%s' "$sha" | cut -c1-7)"
|
||||
if ! docker buildx imagetools inspect "$IMAGE:sha-${short}-cloud" >/dev/null 2>&1; then
|
||||
echo "images for ${current} (sha-${short}) not published yet; its own promotion converges the channel"
|
||||
exit 0
|
||||
fi
|
||||
docker buildx imagetools create -t "$IMAGE:canary" "$IMAGE:sha-${short}"
|
||||
docker buildx imagetools create -t "$IMAGE:canary-cloud" "$IMAGE:sha-${short}-cloud"
|
||||
echo "channel tags moved to canary ${current} (sha-${short})"
|
||||
|
|
|
|||
|
|
@ -335,7 +335,7 @@ jobs:
|
|||
run: node --test ./scripts/__tests__/e2e-shard.test.mjs
|
||||
|
||||
- name: Test release verify workflow wiring
|
||||
run: node --test ./scripts/__tests__/release-verify-workflow.test.mjs
|
||||
run: node --test ./scripts/__tests__/release-verify-workflow.test.mjs ./scripts/cloud-source-verification.test.mjs
|
||||
|
||||
- name: Test standalone package build concurrency
|
||||
run: node --test ./scripts/__tests__/build-standalone-concurrency.test.mjs
|
||||
|
|
@ -553,7 +553,7 @@ jobs:
|
|||
# Preserve the legacy required-check name while the underlying work runs in parallel.
|
||||
name: verify
|
||||
if: ${{ always() }}
|
||||
needs: [gate, policy, typecheck_release_registry, general_tests, build, docker_context_integrity]
|
||||
needs: [gate, policy, typecheck_release_registry, general_tests, verify_paperclip_runner, build, docker_context_integrity]
|
||||
runs-on: ${{ needs.gate.outputs.runner }}
|
||||
timeout-minutes: 5
|
||||
|
||||
|
|
@ -564,6 +564,7 @@ jobs:
|
|||
POLICY_RESULT: ${{ needs.policy.result }}
|
||||
TYPECHECK_RELEASE_REGISTRY_RESULT: ${{ needs.typecheck_release_registry.result }}
|
||||
GENERAL_TESTS_RESULT: ${{ needs.general_tests.result }}
|
||||
RUNNER_VERIFICATION_RESULT: ${{ needs.verify_paperclip_runner.result }}
|
||||
BUILD_RESULT: ${{ needs.build.result }}
|
||||
DOCKER_CONTEXT_INTEGRITY_RESULT: ${{ needs.docker_context_integrity.result }}
|
||||
run: |
|
||||
|
|
@ -572,12 +573,14 @@ jobs:
|
|||
true)
|
||||
test "$TYPECHECK_RELEASE_REGISTRY_RESULT" = "success"
|
||||
test "$GENERAL_TESTS_RESULT" = "success"
|
||||
test "$RUNNER_VERIFICATION_RESULT" = "success"
|
||||
test "$BUILD_RESULT" = "success"
|
||||
test "$DOCKER_CONTEXT_INTEGRITY_RESULT" = "success"
|
||||
;;
|
||||
false)
|
||||
test "$TYPECHECK_RELEASE_REGISTRY_RESULT" = "skipped"
|
||||
test "$GENERAL_TESTS_RESULT" = "skipped"
|
||||
test "$RUNNER_VERIFICATION_RESULT" = "skipped"
|
||||
test "$BUILD_RESULT" = "skipped"
|
||||
test "$DOCKER_CONTEXT_INTEGRITY_RESULT" = "skipped"
|
||||
;;
|
||||
|
|
@ -587,8 +590,8 @@ jobs:
|
|||
;;
|
||||
esac
|
||||
|
||||
build:
|
||||
name: Build
|
||||
verify_paperclip_runner:
|
||||
name: Verify Paperclip Runner
|
||||
needs: [gate, policy]
|
||||
if: ${{ needs.gate.outputs.full_ci == 'true' }}
|
||||
runs-on: ${{ needs.gate.outputs.runner }}
|
||||
|
|
@ -633,6 +636,49 @@ jobs:
|
|||
- name: Verify Paperclip Runner
|
||||
run: pnpm --filter @paperclipai/paperclip-runner check:all
|
||||
|
||||
build:
|
||||
name: Build
|
||||
needs: [gate, policy]
|
||||
if: ${{ needs.gate.outputs.full_ci == 'true' }}
|
||||
runs-on: ${{ needs.gate.outputs.runner }}
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js for pnpm bootstrap
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
||||
env:
|
||||
NPM_CONFIG_AUDIT: "false"
|
||||
NPM_CONFIG_FUND: "false"
|
||||
NPM_CONFIG_UPDATE_NOTIFIER: "false"
|
||||
with:
|
||||
version: 9.15.4
|
||||
|
||||
- name: Restore regenerated PR lockfile (if policy uploaded one)
|
||||
if: needs.policy.outputs.lockfile_regenerated == '1'
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
name: pr-lockfile
|
||||
path: .
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build Runner Evalbook viewer
|
||||
run: pnpm --filter @paperclipai/paperclip-runner build:issue-thread
|
||||
|
||||
|
|
|
|||
|
|
@ -58,16 +58,59 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- group: general-server
|
||||
group_label: server (1/3)
|
||||
# Split the long chat file by collected test locations, and balance
|
||||
# the remaining server files across ten runners. Normal PR/local
|
||||
# invocations retain their complete general-server group.
|
||||
- group: general-server-without-chat
|
||||
group_label: server (1/10)
|
||||
shard_index: 0
|
||||
shard_count: 10
|
||||
- group: general-server-without-chat
|
||||
group_label: server (2/10)
|
||||
shard_index: 1
|
||||
shard_count: 10
|
||||
- group: general-server-without-chat
|
||||
group_label: server (3/10)
|
||||
shard_index: 2
|
||||
shard_count: 10
|
||||
- group: general-server-without-chat
|
||||
group_label: server (4/10)
|
||||
shard_index: 3
|
||||
shard_count: 10
|
||||
- group: general-server-without-chat
|
||||
group_label: server (5/10)
|
||||
shard_index: 4
|
||||
shard_count: 10
|
||||
- group: general-server-without-chat
|
||||
group_label: server (6/10)
|
||||
shard_index: 5
|
||||
shard_count: 10
|
||||
- group: general-server-without-chat
|
||||
group_label: server (7/10)
|
||||
shard_index: 6
|
||||
shard_count: 10
|
||||
- group: general-server-without-chat
|
||||
group_label: server (8/10)
|
||||
shard_index: 7
|
||||
shard_count: 10
|
||||
- group: general-server-without-chat
|
||||
group_label: server (9/10)
|
||||
shard_index: 8
|
||||
shard_count: 10
|
||||
- group: general-server-without-chat
|
||||
group_label: server (10/10)
|
||||
shard_index: 9
|
||||
shard_count: 10
|
||||
- group: general-chat
|
||||
group_label: chat (1/3)
|
||||
shard_index: 0
|
||||
shard_count: 3
|
||||
- group: general-server
|
||||
group_label: server (2/3)
|
||||
- group: general-chat
|
||||
group_label: chat (2/3)
|
||||
shard_index: 1
|
||||
shard_count: 3
|
||||
- group: general-server
|
||||
group_label: server (3/3)
|
||||
- group: general-chat
|
||||
group_label: chat (3/3)
|
||||
shard_index: 2
|
||||
shard_count: 3
|
||||
# Keep parity with pr.yml: workspaces-a is split with Vitest's
|
||||
|
|
@ -191,8 +234,8 @@ jobs:
|
|||
- name: Run deterministic Runner workflow scorer tests
|
||||
run: pnpm test:runner-workflow-evals
|
||||
|
||||
build:
|
||||
name: Build
|
||||
verify_paperclip_runner:
|
||||
name: Verify Paperclip Runner
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
|
|
@ -215,11 +258,62 @@ jobs:
|
|||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Select the pinned Runner Rust toolchain
|
||||
working-directory: packages/paperclip-runner
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rustup show
|
||||
toolchain="$(rustup show active-toolchain | awk '{print $1}')"
|
||||
echo "RUSTUP_TOOLCHAIN=$toolchain" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cache Runner Rust dependencies
|
||||
# Restore and save only within trusted master-push verification. GitHub
|
||||
# isolates branch/PR caches from master; other callers compile afresh.
|
||||
if: ${{ github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
||||
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
workspaces: packages/paperclip-runner/runner -> target
|
||||
shared-key: release-runner-v1
|
||||
# Rebuild workspace code and rerun every check. Cache only compiled
|
||||
# dependencies; never restore installed executables from cargo/bin.
|
||||
cache-workspace-crates: false
|
||||
cache-bin: false
|
||||
# The step guard also restricts restores. Save only after a successful
|
||||
# master-push verification of that push's exact commit.
|
||||
save-if: ${{ github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Verify Paperclip Runner
|
||||
run: pnpm --filter @paperclipai/paperclip-runner check:all
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
||||
with:
|
||||
version: 9.15.4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
name: Release
|
||||
run-name: ${{ inputs.channel == 'preview' && format('Stack deploy {0} build', inputs.request_id) || inputs.channel == 'cloud-migrator' && format('Cloud migrator {0}', inputs.source_ref) || 'Release' }}
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
@ -17,12 +18,22 @@ on:
|
|||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
- preview
|
||||
- cloud-migrator
|
||||
default: stable
|
||||
source_ref:
|
||||
description: (stable) Commit SHA, branch, or tag to publish as stable
|
||||
description: Stable source ref, or full immutable SHA for a preview or cloud migrator build
|
||||
required: true
|
||||
type: string
|
||||
default: master
|
||||
request_id:
|
||||
description: (preview/cloud-migrator) Correlation UUID
|
||||
type: string
|
||||
default: ""
|
||||
preview_migrator:
|
||||
description: (preview) Publish isolated shared and database packages if missing
|
||||
type: boolean
|
||||
default: false
|
||||
stable_date:
|
||||
description: Enter a UTC date in YYYY-MM-DD format, for example 2026-03-18. Do not enter a version string. The workflow will resolve that date to a stable version such as 2026.318.0, then 2026.318.1 for the next same-day stable.
|
||||
required: false
|
||||
|
|
@ -46,7 +57,7 @@ on:
|
|||
default: false
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.event_name }}-${{ github.ref }}
|
||||
group: ${{ (inputs.channel == 'preview' || inputs.channel == 'cloud-migrator') && format('{0}-{1}', inputs.channel, inputs.source_ref) || format('release-{0}-{1}', github.event_name, github.ref) }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
|
|
@ -64,11 +75,247 @@ env:
|
|||
NPM_PUBLISH_VERIFY_DELAY_SECONDS: "10"
|
||||
|
||||
jobs:
|
||||
plan_preview:
|
||||
name: Check preview artifacts
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'workflow_dispatch' && (inputs.channel == 'preview' || inputs.channel == 'cloud-migrator') && !inputs.dry_run
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
image: ${{ steps.plan.outputs.image }}
|
||||
packages: ${{ steps.plan.outputs.packages }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Validate immutable source and inspect existing artifacts
|
||||
id: plan
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_ref }}
|
||||
REQUEST_ID: ${{ inputs.request_id }}
|
||||
PREVIEW_MIGRATOR: ${{ inputs.preview_migrator }}
|
||||
PLAN_COMMAND: ${{ inputs.channel == 'cloud-migrator' && 'plan-migrator' || 'plan' }}
|
||||
run: node scripts/preview-artifacts.mjs "$PLAN_COMMAND" "$SOURCE_SHA" "$REQUEST_ID" "$PREVIEW_MIGRATOR"
|
||||
|
||||
package_preview:
|
||||
name: Build preview migrator
|
||||
needs: plan_preview
|
||||
if: needs.plan_preview.outputs.packages == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
path: trusted
|
||||
persist-credentials: false
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ inputs.source_ref }}
|
||||
path: source
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
||||
with:
|
||||
version: 9.15.4
|
||||
run_install: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Install build dependencies without lifecycle scripts
|
||||
working-directory: source
|
||||
run: pnpm install --ignore-scripts --no-frozen-lockfile
|
||||
- name: Build and pack exact-source preview packages
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_ref }}
|
||||
run: node trusted/scripts/preview-artifacts.mjs pack source packages "$SOURCE_SHA"
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: preview-packages
|
||||
overwrite: true
|
||||
path: packages/*.tgz
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
publish_preview:
|
||||
name: Publish preview migrator
|
||||
needs: [plan_preview, package_preview]
|
||||
if: github.ref == 'refs/heads/master' && needs.plan_preview.outputs.packages == 'true' && needs.package_preview.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
# A manual preview and a merge-triggered migrator may compile in parallel.
|
||||
# Serialize only publication so they cannot race an immutable npm version,
|
||||
# without making the migrator wait for a preview's separate image build.
|
||||
concurrency:
|
||||
group: preview-package-publish-${{ inputs.source_ref }}
|
||||
cancel-in-progress: false
|
||||
# Reuse release.yml's established npm trusted-publisher identity. This job
|
||||
# publishes only isolated preview versions; it cannot advance lane tags.
|
||||
environment: npm-canary
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Install npm with trusted publishing support
|
||||
run: npm install --global npm@11.18.0 --ignore-scripts
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: preview-packages
|
||||
path: preview-packages
|
||||
- name: Publish immutable preview packages without running package code
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_ref }}
|
||||
run: node scripts/preview-artifacts.mjs publish preview-packages "$SOURCE_SHA"
|
||||
|
||||
image_preview:
|
||||
name: Build preview cloud image
|
||||
needs: plan_preview
|
||||
if: needs.plan_preview.outputs.image == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ inputs.source_ref }}
|
||||
persist-credentials: false
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
||||
with:
|
||||
version: 9.15.4
|
||||
run_install: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Prepare locked image context
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test "$(git rev-parse HEAD)" = "$SOURCE_SHA"
|
||||
pnpm install --resolution-only --ignore-scripts --ignore-pnpmfile --no-frozen-lockfile
|
||||
echo "TOOLS_EPOCH=$(date -u +%G-W%V)" >> "$GITHUB_ENV"
|
||||
- uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
||||
- name: Build the immutable cloud image without registry credentials
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
with:
|
||||
context: .
|
||||
target: cloud
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
provenance: false # Docker archives cannot carry registry attestations.
|
||||
outputs: type=docker,dest=${{ runner.temp }}/preview-image.tar
|
||||
tags: ghcr.io/paperclipai/paperclip:sha-${{ inputs.source_ref }}-cloud
|
||||
build-args: |
|
||||
CLOUD_BUNDLED_PLUGINS=daytona
|
||||
CLOUD_BUNDLED_SERVER_DEPS=@sentry/node
|
||||
PAPERCLIP_BUILD_COMMIT=${{ inputs.source_ref }}
|
||||
PAPERCLIP_BUILD_VERSION=0.0.0-preview.g${{ inputs.source_ref }}
|
||||
CLI_TOOLS_CACHE_EPOCH=${{ env.TOOLS_EPOCH }}
|
||||
labels: |
|
||||
org.opencontainers.image.revision=${{ inputs.source_ref }}
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: preview-image
|
||||
overwrite: true
|
||||
path: ${{ runner.temp }}/preview-image.tar
|
||||
compression-level: 0
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
publish_image_preview:
|
||||
name: Publish preview cloud image
|
||||
needs: [plan_preview, image_preview]
|
||||
if: github.ref == 'refs/heads/master' && needs.plan_preview.outputs.image == 'true' && needs.image_preview.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
# This existing environment has an external master-only branch policy.
|
||||
environment: npm-canary
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: preview-image
|
||||
path: preview-image
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Verify image identity and publish without executing image code
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_ref }}
|
||||
run: node scripts/preview-artifacts.mjs publish-image preview-image/preview-image.tar "$SOURCE_SHA"
|
||||
|
||||
result_preview:
|
||||
name: Verify preview artifacts
|
||||
needs: [plan_preview, image_preview, publish_image_preview, package_preview, publish_preview]
|
||||
if: >-
|
||||
always() && inputs.channel == 'preview' && needs.plan_preview.result == 'success' &&
|
||||
(needs.publish_image_preview.result == 'success' || needs.plan_preview.outputs.image == 'false') &&
|
||||
(needs.publish_preview.result == 'success' || needs.plan_preview.outputs.packages == 'false')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Confirm exact artifacts are visible
|
||||
env:
|
||||
SOURCE_SHA: ${{ inputs.source_ref }}
|
||||
REQUEST_ID: ${{ inputs.request_id }}
|
||||
PREVIEW_MIGRATOR: ${{ inputs.preview_migrator }}
|
||||
run: node scripts/preview-artifacts.mjs result "$SOURCE_SHA" "$REQUEST_ID"
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: stack-deploy-result
|
||||
overwrite: true
|
||||
path: stack-deploy-result/result.json
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
verify_canary:
|
||||
if: github.event_name == 'push'
|
||||
uses: ./.github/workflows/release-verify.yml
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
name: Reuse exact-source verification
|
||||
if: github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 50
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
- name: Require successful source checks for this exact master push
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
SOURCE_SHA: ${{ github.sha }}
|
||||
run: node scripts/cloud-source-verification.mjs "$SOURCE_SHA"
|
||||
|
||||
publish_canary:
|
||||
if: github.event_name == 'push'
|
||||
|
|
@ -81,6 +328,8 @@ jobs:
|
|||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
# For the explicit docker.yml dispatch below.
|
||||
actions: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
|
@ -139,6 +388,29 @@ jobs:
|
|||
git push origin "refs/tags/${tag}"
|
||||
echo "version=${tag#canary/v}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Canary images previously relied on the master-push docker.yml run,
|
||||
# whose single pending concurrency slot gets superseded by every
|
||||
# newer push — on a busy day no canary image publishes at all (five
|
||||
# consecutive canaries shipped npm packages with no cloud image on
|
||||
# 2026-09-06, starving downstream managed deploys for ~18 hours).
|
||||
# Tag pushes made with GITHUB_TOKEN do not fire docker.yml's
|
||||
# triggers, so dispatch the image build at the canary tag
|
||||
# explicitly, exactly like the nightly and beta lanes: the run keys
|
||||
# its concurrency off the tag ref, so no master push can supersede
|
||||
# it, and docker.yml's `type=sha` mapping publishes the
|
||||
# sha-<short> and sha-<short>-cloud images either way.
|
||||
- name: Build Docker images for the canary tag
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
{
|
||||
echo "## Canary published"
|
||||
echo ""
|
||||
echo "- Published canary: \`${{ steps.canary_tag.outputs.version }}\`"
|
||||
echo "- Docker build dispatched at \`canary/v${{ steps.canary_tag.outputs.version }}\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
gh workflow run docker.yml --ref "refs/tags/canary/v${{ steps.canary_tag.outputs.version }}" --repo "$GITHUB_REPOSITORY"
|
||||
|
||||
# The package is already public when this gate runs. A red result leaves the
|
||||
# immutable canary in npm, but makes the release workflow visibly fail before
|
||||
# anyone mistakes an installable package for an onboardable one.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ on:
|
|||
type: string
|
||||
|
||||
concurrency:
|
||||
group: runner-chaos-evals-${{ inputs.ref || github.ref }}
|
||||
# Reusable calls inherit the caller's workflow name. Cloud readiness and
|
||||
# Release verify the same SHA independently and must not cancel each other.
|
||||
group: runner-chaos-evals-${{ github.workflow }}-${{ inputs.ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
name: Storybook Deploy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "Repository branch to publish (empty uses the selected workflow branch)"
|
||||
type: string
|
||||
default: ""
|
||||
# Also exposed by Storybook Visual, which is already available on master.
|
||||
workflow_call:
|
||||
inputs:
|
||||
branch:
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
authorize:
|
||||
name: Authorize Storybook publisher
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
sha: ${{ steps.source.outputs.sha }}
|
||||
branch: ${{ steps.source.outputs.branch }}
|
||||
branch_key: ${{ steps.source.outputs.branch_key }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Require CODEOWNER initiator and rerunner
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const authorize = require('./.github/scripts/authorize-storybook-deploy.cjs');
|
||||
await authorize({ github, context });
|
||||
|
||||
- name: Pin requested repository branch
|
||||
id: source
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
env:
|
||||
SOURCE_BRANCH: ${{ inputs.branch }}
|
||||
STORYBOOK_S3_BUCKET: ${{ vars.STORYBOOK_S3_BUCKET }}
|
||||
STORYBOOK_PUBLIC_BASE_URL: ${{ vars.STORYBOOK_PUBLIC_BASE_URL }}
|
||||
with:
|
||||
script: |
|
||||
const branch = process.env.SOURCE_BRANCH || context.ref.slice('refs/heads/'.length);
|
||||
const { data } = await github.rest.git.getRef({ ...context.repo, ref: `heads/${branch}` });
|
||||
if (data.ref !== `refs/heads/${branch}` || data.object.type !== 'commit') {
|
||||
throw new Error('Select an existing branch in this repository.');
|
||||
}
|
||||
// With no source override, preserve the exact dispatched commit.
|
||||
const sha = process.env.SOURCE_BRANCH ? data.object.sha : context.sha;
|
||||
const { storybookDestination } = require('./.github/scripts/storybook-destination.cjs');
|
||||
const destination = storybookDestination({ branch, sha,
|
||||
runId: context.runId, runAttempt: process.env.GITHUB_RUN_ATTEMPT,
|
||||
bucket: process.env.STORYBOOK_S3_BUCKET, baseUrl: process.env.STORYBOOK_PUBLIC_BASE_URL });
|
||||
core.setOutput('sha', sha);
|
||||
core.setOutput('branch_key', destination.branchKey);
|
||||
core.setOutput('branch', branch);
|
||||
|
||||
build:
|
||||
name: Build selected branch Storybook
|
||||
permissions: {}
|
||||
needs: authorize
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
outputs:
|
||||
artifact_name: ${{ steps.artifact.outputs.name }}
|
||||
env:
|
||||
STORYBOOK_DISABLE_TELEMETRY: "1"
|
||||
steps:
|
||||
- name: Download public source without repository credentials
|
||||
env:
|
||||
SOURCE_SHA: ${{ needs.authorize.outputs.sha }}
|
||||
run: |
|
||||
[[ "$SOURCE_SHA" =~ ^[a-f0-9]{40}$ ]]
|
||||
curl --fail --silent --show-error --location --retry 3 \
|
||||
"https://codeload.github.com/paperclipai/paperclip/tar.gz/$SOURCE_SHA" \
|
||||
--output "$RUNNER_TEMP/source.tar.gz"
|
||||
tar -xzf "$RUNNER_TEMP/source.tar.gz" --strip-components=1
|
||||
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
|
||||
with:
|
||||
version: 9.15.4
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: 24
|
||||
package-manager-cache: false
|
||||
- run: pnpm install --frozen-lockfile --ignore-scripts
|
||||
- run: pnpm build-storybook
|
||||
- name: Record source and validate output
|
||||
id: artifact
|
||||
env:
|
||||
SOURCE_SHA: ${{ needs.authorize.outputs.sha }}
|
||||
SOURCE_BRANCH: ${{ needs.authorize.outputs.branch }}
|
||||
run: |
|
||||
test -s ui/storybook-static/index.html
|
||||
test -s ui/storybook-static/iframe.html
|
||||
test -s ui/storybook-static/index.json
|
||||
jq -n --arg sha "$SOURCE_SHA" --arg branch "$SOURCE_BRANCH" \
|
||||
'{sha: $sha, branch: $branch}' > ui/storybook-static/deployment.json
|
||||
echo "name=storybook-deploy-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: ${{ steps.artifact.outputs.name }}
|
||||
path: ui/storybook-static
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
deploy:
|
||||
name: Publish branch Storybook to S3
|
||||
needs: [authorize, build]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
concurrency:
|
||||
group: storybook-deploy-${{ needs.authorize.outputs.branch_key }}
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
environment:
|
||||
name: storybook-deploy
|
||||
url: ${{ steps.deployment.outputs.url }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: .github/scripts
|
||||
- name: Recheck CODEOWNER access before publishing
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const authorize = require('./.github/scripts/authorize-storybook-deploy.cjs');
|
||||
await authorize({ github, context });
|
||||
- name: Download the successful build artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
name: ${{ needs.build.outputs.artifact_name }}
|
||||
path: storybook-static
|
||||
- name: Assume the Storybook-only uploader role
|
||||
uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6
|
||||
with:
|
||||
role-to-assume: ${{ vars.STORYBOOK_AWS_ROLE_ARN }}
|
||||
aws-region: ${{ vars.STORYBOOK_AWS_REGION }}
|
||||
role-duration-seconds: 900
|
||||
- name: Publish this branch preview
|
||||
id: deployment
|
||||
env:
|
||||
SOURCE_SHA: ${{ needs.authorize.outputs.sha }}
|
||||
SOURCE_BRANCH: ${{ needs.authorize.outputs.branch }}
|
||||
STORYBOOK_S3_BUCKET: ${{ vars.STORYBOOK_S3_BUCKET }}
|
||||
STORYBOOK_PUBLIC_BASE_URL: ${{ vars.STORYBOOK_PUBLIC_BASE_URL }}
|
||||
run: node .github/scripts/publish-storybook.cjs
|
||||
- name: Verify public build and stable branch URL
|
||||
env:
|
||||
BUILD_URL: ${{ steps.deployment.outputs.build_url }}
|
||||
BRANCH_URL: ${{ steps.deployment.outputs.url }}
|
||||
SOURCE_SHA: ${{ needs.authorize.outputs.sha }}
|
||||
run: node .github/scripts/verify-storybook.cjs
|
||||
- name: Upload deployment links
|
||||
if: ${{ !cancelled() && steps.deployment.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: storybook-deployment-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: ${{ steps.deployment.outputs.report_path }}
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
|
@ -3,6 +3,16 @@ name: Storybook Visual
|
|||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "Repository branch to publish (deployment only; empty uses the selected workflow branch)"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
deploy_preview:
|
||||
description: "Publish this branch to S3/CloudFront instead of running visual tests (CODEOWNERS only)"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
update_snapshots:
|
||||
description: "Generate updated snapshots and a baseline review bundle"
|
||||
required: false
|
||||
|
|
@ -18,7 +28,7 @@ on:
|
|||
- labeled
|
||||
|
||||
concurrency:
|
||||
group: storybook-visual-${{ github.event.pull_request.number || github.ref }}
|
||||
group: storybook-visual-${{ inputs.deploy_preview && github.run_id || 'visual' }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
|
|
@ -28,7 +38,7 @@ jobs:
|
|||
visual:
|
||||
name: Storybook visual regression
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event_name == 'workflow_dispatch' && !inputs.deploy_preview) ||
|
||||
(github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'storybook-visual'))
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 35
|
||||
|
|
@ -100,3 +110,13 @@ jobs:
|
|||
path: tests/storybook-visual/baseline-review/snapshots.tgz
|
||||
retention-days: 30
|
||||
if-no-files-found: error
|
||||
|
||||
preview:
|
||||
name: Deploy selected branch Storybook
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.deploy_preview
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/storybook-deploy.yml
|
||||
with:
|
||||
branch: ${{ inputs.branch }}
|
||||
|
|
|
|||
10
DESIGN.md
10
DESIGN.md
|
|
@ -35,6 +35,16 @@ Existing tiers already in index.css (~80+ tokens) — extraction maps to these o
|
|||
7. **Words are part of the system.** One name per concept across the entire UI — the canonical term is *task* (never *issue* or *ticket* in copy, labels, or empty states). Buttons name the action ("Approve hire," not "Submit"). Errors say what happened and what to do. Empty states say what to do first. **Note:** enforcing the task rename is a visible change and is explicitly OUT of the zero-visual-change extraction run; it happens in its own follow-up run.
|
||||
8. **Agent-modifiable by design.** The system must be changeable via instructions: single token source, lint rules that enforce it, and this document kept current. A correct change should be expressible as "edit tokens + run checks," not "visit 40 files."
|
||||
|
||||
## Contextual feedback
|
||||
|
||||
Do not show a toast for task or run state already visible on the current screen.
|
||||
This includes descendant runs represented by the open subtree. Show local action
|
||||
results in place; keep failures actionable inline. Notifications for other work
|
||||
remain useful. Expected cancellation is neutral gray, not an error. A paused task replaces the composer with an amber takeover. It says “Task is
|
||||
paused.” and “Resume this task to send a message.” with a “Resume task” action.
|
||||
Subtrees use “Subtree is paused.” and “Resume subtree.” The takeover cannot be
|
||||
dismissed, retains drafts, and hides message inputs until the pause is released.
|
||||
|
||||
## Enforcement (what "compliant" means for the extraction run)
|
||||
|
||||
- **Zero visual change is proven, not promised:** Storybook visual snapshots are baselined before any refactor, and all snapshots match baseline after it. A change that alters rendered output must be intentional and human-approved.
|
||||
|
|
|
|||
40
Dockerfile
40
Dockerfile
|
|
@ -51,7 +51,7 @@ COPY scripts/link-plugin-dev-sdk.mjs scripts/
|
|||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
FROM base AS build
|
||||
FROM base AS rust-toolchain
|
||||
WORKDIR /app
|
||||
# Debian's packaged rust lags the ecosystem (trixie ships 1.85) and the
|
||||
# runner's dependency tree now requires a newer rustc. Install rustup from a
|
||||
|
|
@ -83,8 +83,31 @@ RUN set -eux; \
|
|||
chmod +x /tmp/rustup-init; \
|
||||
/tmp/rustup-init -y --no-modify-path --profile minimal --default-toolchain none; \
|
||||
rm /tmp/rustup-init
|
||||
# Install the package-owned compiler before any application source enters the
|
||||
# stage. rustup-init above installs rustup itself, not the selected compiler.
|
||||
COPY packages/paperclip-runner/rust-toolchain.toml /tmp/runner-toolchain/rust-toolchain.toml
|
||||
RUN cd /tmp/runner-toolchain && rustup show
|
||||
|
||||
FROM rust-toolchain AS runner-build
|
||||
WORKDIR /app/packages/paperclip-runner
|
||||
# Rust embeds protocol schemas and fixtures with include_str!. Keep those
|
||||
# alongside the complete Cargo workspace so every compile-time input keys
|
||||
# this layer. Ordinary server/UI edits can then reuse the native build.
|
||||
COPY packages/paperclip-runner/rust-toolchain.toml ./
|
||||
COPY packages/paperclip-runner/runner ./runner
|
||||
COPY packages/paperclip-runner/protocol ./protocol
|
||||
# Cargo fingerprints source mtimes. Normalize them here and after the full
|
||||
# source copy below so a fresh checkout cannot invalidate unchanged inputs.
|
||||
RUN find runner protocol -type f -exec touch -d @0 {} + \
|
||||
&& touch -d @0 rust-toolchain.toml \
|
||||
&& cargo build --release --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --bin paperclip-runnerd
|
||||
|
||||
FROM runner-build AS build
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app /app
|
||||
COPY . .
|
||||
RUN find packages/paperclip-runner/runner packages/paperclip-runner/protocol -type f -exec touch -d @0 {} + \
|
||||
&& touch -d @0 packages/paperclip-runner/rust-toolchain.toml
|
||||
RUN pnpm --filter @paperclipai/ui build
|
||||
RUN pnpm --filter @paperclipai/plugin-sdk build
|
||||
# The server build runs scripts/write-build-stamp.mjs, which stamps the built
|
||||
|
|
@ -103,14 +126,6 @@ RUN rm -rf packages/paperclip-runner/runner/target
|
|||
FROM base AS production
|
||||
ARG USER_UID=1000
|
||||
ARG USER_GID=1000
|
||||
# Real version for this build, computed from `git describe` on the CI runner
|
||||
# (the image has no .git, so the server cannot derive it at runtime). Empty for
|
||||
# local `docker build`, which just leaves the server on its normal fallbacks.
|
||||
ARG PAPERCLIP_BUILD_VERSION=""
|
||||
# The exact commit this image was built from, for the same reason: server-info
|
||||
# falls back to PAPERCLIP_BUILD_COMMIT when git is unavailable, which feeds the
|
||||
# /api/health `commit` field that deploy tooling verifies. Empty locally.
|
||||
ARG PAPERCLIP_BUILD_COMMIT=""
|
||||
# Refreshes the tool layer below when it changes (CI stamps an ISO week, so
|
||||
# the @latest CLI tools advance weekly). Without it the cached layer would
|
||||
# freeze the tools until an unrelated cache bust.
|
||||
|
|
@ -133,6 +148,13 @@ RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|||
|
||||
COPY --chown=node:node --from=build /app /app
|
||||
|
||||
# Declare per-build metadata after the stable RUN layers. Docker includes
|
||||
# in-scope ARG values in a RUN's environment even when its command does not
|
||||
# mention them; declaring these earlier invalidates the weekly tool cache.
|
||||
# The build stage still receives the commit before writing dist/build-info.json.
|
||||
# Empty for local builds, preserving the server's normal version fallbacks.
|
||||
ARG PAPERCLIP_BUILD_VERSION=""
|
||||
ARG PAPERCLIP_BUILD_COMMIT=""
|
||||
ENV NODE_ENV=production \
|
||||
HOME=/paperclip \
|
||||
HOST=0.0.0.0 \
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import fs from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
|
@ -121,6 +122,29 @@ describe("managed install store", () => {
|
|||
expect(fs.statSync(rcPath).mode & 0o777).toBe(0o640);
|
||||
});
|
||||
|
||||
it("uses the pinned Node for child tools even with an older node first on the service PATH", () => {
|
||||
const entrypoint = path.join(paths.currentPath, "node_modules", "paperclipai", "dist", "index.js");
|
||||
fs.mkdirSync(path.dirname(entrypoint), { recursive: true });
|
||||
fs.writeFileSync(entrypoint, `console.log(require("node:child_process").execFileSync("node", ["-p", "process.execPath"], {encoding: "utf8"}).trim())`);
|
||||
const oldBin = path.join(root, "old-bin");
|
||||
fs.mkdirSync(oldBin);
|
||||
fs.writeFileSync(path.join(oldBin, "node"), "#!/bin/sh\nexit 42\n", { mode: 0o755 });
|
||||
writeManagedShim(paths);
|
||||
const output = execFileSync(paths.shimPath, [], { env: { ...process.env, PATH: oldBin }, encoding: "utf8" });
|
||||
expect(fs.realpathSync(output.trim())).toBe(fs.realpathSync(process.execPath));
|
||||
expect(removeManagedShim(paths)).toBe(true);
|
||||
});
|
||||
|
||||
it("upgrades and removes the original managed shim format", () => {
|
||||
writeManagedShim(paths);
|
||||
const original = fs.readFileSync(paths.shimPath, "utf8").split("\n").filter((line) => !line.startsWith("export PATH=")).join("\n");
|
||||
fs.writeFileSync(paths.shimPath, original);
|
||||
writeManagedShim(paths);
|
||||
expect(fs.readFileSync(paths.shimPath, "utf8")).toContain("export PATH=");
|
||||
fs.writeFileSync(paths.shimPath, original);
|
||||
expect(removeManagedShim(paths)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects marker substrings that are not the exact managed shim format", () => {
|
||||
fs.mkdirSync(path.dirname(paths.shimPath), { recursive: true });
|
||||
fs.writeFileSync(paths.shimPath, `#!/bin/sh\necho '${MANAGED_SHIM_MARKER}'\n`);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const PRODUCT_ID = "77777777-7777-4777-8777-777777777777";
|
|||
const INTERACTION_ID = "88888888-8888-4888-8888-888888888888";
|
||||
const HOLD_ID = "99999999-9999-4999-8999-999999999999";
|
||||
const ATTACHMENT_ID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
|
||||
const SECOND_ATTACHMENT_ID = "abababab-abab-4bab-8bab-abababababab";
|
||||
const LABEL_ID = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
|
||||
|
||||
function createProgram(): Command {
|
||||
|
|
@ -61,6 +62,24 @@ describe("issue subresource commands", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("binds explicit uploaded attachments when adding a comment", async () => {
|
||||
const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(jsonResponse()));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
await run([
|
||||
"issue", "comment", ISSUE_ID,
|
||||
"--body", "The requested files are ready.",
|
||||
"--attachment-id", ATTACHMENT_ID, SECOND_ATTACHMENT_ID,
|
||||
]);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock.mock.calls[0]?.[0]).toBe(`http://localhost:3100/api/issues/${ISSUE_ID}/comments`);
|
||||
expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
|
||||
body: "The requested files are ready.",
|
||||
attachmentIds: [ATTACHMENT_ID, SECOND_ATTACHMENT_ID],
|
||||
});
|
||||
});
|
||||
|
||||
it("wraps comments, approvals, markers, and recovery action endpoints", async () => {
|
||||
const fetchMock = vi
|
||||
.fn()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import fs from "node:fs";
|
|||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { flipCurrentAtomic, initializeInstallStore, payloadPathFor, readInstallManifest, resolveInstallStorePaths, writeInstallManifestAtomic, type InstallManifest, type InstallRecord } from "../install-store.js";
|
||||
import { writeManagedShim, flipCurrentAtomic, initializeInstallStore, payloadPathFor, readInstallManifest, resolveInstallStorePaths, writeInstallManifestAtomic, type InstallManifest, type InstallRecord } from "../install-store.js";
|
||||
import type { CommandRunner } from "../commands/install.js";
|
||||
import { compareVersions, detectInstallMode, resolveUpdateRequest, rollbackManagedInstall, updateCommand } from "../commands/update.js";
|
||||
|
||||
|
|
@ -36,6 +36,56 @@ afterEach(() => {
|
|||
});
|
||||
|
||||
describe("update command", () => {
|
||||
it.each(["npm", "git", "global-npm"] as const)("rejects %s updates on unsupported Node before any update work", async (source) => {
|
||||
const paths = resolveInstallStorePaths(); initializeInstallStore(paths);
|
||||
const payload = payloadPathFor(paths, "npm", "1.0.0");
|
||||
const entrypoint = createPayload(payload, "1.0.0");
|
||||
flipCurrentAtomic(payload, paths);
|
||||
const manifest: InstallManifest = { schemaVersion: 1, ...record(payload, "1.0.0"), source: source === "git" ? "git" : "npm", previous: [] };
|
||||
writeInstallManifestAtomic(manifest, paths);
|
||||
const runCommand = vi.fn<CommandRunner>();
|
||||
const backup = vi.fn();
|
||||
const restartActiveService = vi.fn();
|
||||
const nodeVersion = Object.getOwnPropertyDescriptor(process.versions, "node")!;
|
||||
Object.defineProperty(process.versions, "node", { ...nodeVersion, value: "22.22.2" });
|
||||
try {
|
||||
await expect(updateCommand({ yes: true }, {
|
||||
paths,
|
||||
executablePath: source === "global-npm" ? path.join(root, "lib", "node_modules", "paperclipai", "dist", "index.js") : entrypoint,
|
||||
runCommand, backup, restartActiveService,
|
||||
})).rejects.toThrow("npx paperclipai@latest install --yes");
|
||||
expect(runCommand).not.toHaveBeenCalled();
|
||||
expect(backup).not.toHaveBeenCalled();
|
||||
expect(restartActiveService).not.toHaveBeenCalled();
|
||||
expect(readInstallManifest(paths)).toEqual(manifest);
|
||||
expect(fs.realpathSync(paths.currentPath)).toBe(fs.realpathSync(payload));
|
||||
} finally {
|
||||
Object.defineProperty(process.versions, "node", nodeVersion);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps update checks, dry runs, and rollback available on unsupported Node", async () => {
|
||||
const paths = resolveInstallStorePaths(); initializeInstallStore(paths);
|
||||
const oldPayload = payloadPathFor(paths, "npm", "1.0.0"); createPayload(oldPayload, "1.0.0");
|
||||
const payload = payloadPathFor(paths, "npm", "2.0.0"); const executablePath = createPayload(payload, "2.0.0");
|
||||
flipCurrentAtomic(payload, paths);
|
||||
writeInstallManifestAtomic({ schemaVersion: 1, ...record(payload, "2.0.0"), previous: [record(oldPayload, "1.0.0")] }, paths);
|
||||
const runCommand = vi.fn(async () => ({ stdout: '\"3.0.0\"\n', stderr: "" }));
|
||||
const restartActiveService = vi.fn(async () => false);
|
||||
const nodeVersion = Object.getOwnPropertyDescriptor(process.versions, "node")!;
|
||||
Object.defineProperty(process.versions, "node", { ...nodeVersion, value: "22.22.2" });
|
||||
try {
|
||||
await updateCommand({ check: true }, { paths, executablePath, runCommand });
|
||||
await updateCommand({ dryRun: true }, { paths, executablePath, runCommand });
|
||||
expect(readInstallManifest(paths)?.version).toBe("2.0.0");
|
||||
await updateCommand({ rollback: true }, { paths, executablePath, restartActiveService });
|
||||
expect(readInstallManifest(paths)?.version).toBe("1.0.0");
|
||||
expect(restartActiveService).toHaveBeenCalledWith("1.0.0");
|
||||
} finally {
|
||||
Object.defineProperty(process.versions, "node", nodeVersion);
|
||||
}
|
||||
});
|
||||
|
||||
it("orders SemVer prerelease identifiers numerically", () => {
|
||||
expect(compareVersions("1.0.0-canary.10", "1.0.0-canary.2")).toBeGreaterThan(0);
|
||||
expect(compareVersions("1.0.0-1", "1.0.0-alpha")).toBeLessThan(0);
|
||||
|
|
@ -72,12 +122,16 @@ describe("update command", () => {
|
|||
fs.writeFileSync(path.join(newPayload, "node_modules", "paperclipai", "package.json"), JSON.stringify({ version: "0.3.1" }));
|
||||
flipCurrentAtomic(oldPayload, paths);
|
||||
writeInstallManifestAtomic({ schemaVersion: 1, source: "git", version: "0.3.1", channel: "pinned", repo: "paperclipai/paperclip", ref: "master", sha: oldSha, payloadPath: oldPayload, installedAt: "2026-07-22T00:00:00.000Z", previous: [] }, paths);
|
||||
writeManagedShim(paths);
|
||||
// Simulate a launcher generated before child-runtime PATH pinning existed.
|
||||
fs.writeFileSync(paths.shimPath, fs.readFileSync(paths.shimPath, "utf8").replace(/^export PATH=.*\n/m, ""));
|
||||
const backup = vi.fn(async () => undefined);
|
||||
const confirm = vi.fn(async () => true);
|
||||
const restartActiveService = vi.fn(async () => true);
|
||||
const runCommand = vi.fn(async (file: string) => file === "curl" ? { stdout: JSON.stringify({ sha: newSha }), stderr: "" } : { stdout: "0.3.1\n", stderr: "" });
|
||||
await updateCommand({}, { paths, executablePath: executable, runCommand, backup, confirm, restartActiveService, hasInstanceData: () => true, now: () => new Date("2026-07-22T12:00:00Z") });
|
||||
expect(confirm).toHaveBeenCalledWith(expect.stringContaining(`commit ${newSha.slice(0, 12)}`));
|
||||
expect(fs.readFileSync(paths.shimPath, "utf8")).toContain(`export PATH='${path.dirname(process.execPath)}'`);
|
||||
expect(backup).toHaveBeenCalledOnce();
|
||||
expect(restartActiveService).toHaveBeenCalledWith("0.3.1");
|
||||
expect(readInstallManifest(paths)?.sha).toBe(newSha);
|
||||
|
|
@ -134,6 +188,9 @@ describe("update command", () => {
|
|||
const paths = resolveInstallStorePaths(); initializeInstallStore(paths);
|
||||
const oldPayload = payloadPathFor(paths, "npm", "1.0.0"); const executable = createPayload(oldPayload, "1.0.0"); flipCurrentAtomic(oldPayload, paths);
|
||||
writeInstallManifestAtomic({ schemaVersion: 1, ...record(oldPayload, "1.0.0"), previous: [] }, paths);
|
||||
writeManagedShim(paths);
|
||||
// Simulate a launcher generated before child-runtime PATH pinning existed.
|
||||
fs.writeFileSync(paths.shimPath, fs.readFileSync(paths.shimPath, "utf8").replace(/^export PATH=.*\n/m, ""));
|
||||
const backup = vi.fn(async () => undefined);
|
||||
const restartActiveService = vi.fn(async () => true);
|
||||
const runCommand = vi.fn(async (file: string, args: string[]) => {
|
||||
|
|
@ -142,6 +199,7 @@ describe("update command", () => {
|
|||
return { stdout: "2.0.0\n", stderr: "" };
|
||||
});
|
||||
await updateCommand({}, { paths, executablePath: executable, runCommand, backup, restartActiveService, hasInstanceData: () => true, now: () => new Date("2026-07-22T12:00:00Z") });
|
||||
expect(fs.readFileSync(paths.shimPath, "utf8")).toContain(`export PATH='${path.dirname(process.execPath)}'`);
|
||||
expect(backup).toHaveBeenCalledOnce();
|
||||
expect(restartActiveService).toHaveBeenCalledWith("2.0.0");
|
||||
expect(readInstallManifest(paths)?.version).toBe("2.0.0");
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { execFileSync } from "node:child_process";
|
|||
import { randomUUID } from "node:crypto";
|
||||
import { createServer } from "node:net";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||
import { afterEach, describe, expect, it, onTestFinished, vi } from "vitest";
|
||||
import {
|
||||
agents,
|
||||
|
|
@ -13,6 +15,8 @@ import {
|
|||
companies,
|
||||
companyMemberships,
|
||||
createDb,
|
||||
closeRegisteredClients,
|
||||
ensurePostgresDatabase,
|
||||
executionWorkspaces,
|
||||
inspectMigrations,
|
||||
issueComments,
|
||||
|
|
@ -1644,17 +1648,32 @@ describe("worktree helpers", () => {
|
|||
const sourceEnvPath = path.join(sourceConfigDir, ".env");
|
||||
const sourceKeyPath = path.join(sourceConfigDir, "secrets", "master.key");
|
||||
const worktreeHome = path.join(tempRoot, ".paperclip-worktrees");
|
||||
const sourceDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-auth-source-");
|
||||
onTestFinished(() => sourceDb.cleanup());
|
||||
|
||||
await seedValidWorktreeSource(sourceDb.connectionString);
|
||||
const sourceCluster = await startEmbeddedPostgresTestDatabase("paperclip-worktree-auth-source-");
|
||||
const sourceUrl = new URL(sourceCluster.connectionString);
|
||||
sourceUrl.pathname = "/lagging_source";
|
||||
const sourceDb = { connectionString: sourceUrl.toString() };
|
||||
onTestFinished(async () => {
|
||||
await closeRegisteredClients(sourceDb.connectionString);
|
||||
await sourceCluster.cleanup();
|
||||
});
|
||||
await ensurePostgresDatabase(sourceCluster.connectionString, "lagging_source");
|
||||
// A lagging source must also have the prior schema. Deleting only the
|
||||
// newest receipt from a fully migrated schema relied on that particular
|
||||
// migration being idempotent and breaks when the new migration creates a
|
||||
// table. Build the actual all-but-last schema before shuffling its history.
|
||||
const migrationsRoot = new URL("../../../packages/db/src/migrations/", import.meta.url);
|
||||
const journal = JSON.parse(fs.readFileSync(new URL("meta/_journal.json", migrationsRoot), "utf8"));
|
||||
const priorEntries = journal.entries.slice(0, -1);
|
||||
const priorMigrations = path.join(tempRoot, "prior-migrations");
|
||||
fs.mkdirSync(path.join(priorMigrations, "meta"), { recursive: true });
|
||||
fs.writeFileSync(path.join(priorMigrations, "meta", "_journal.json"), JSON.stringify({ ...journal, entries: priorEntries }));
|
||||
for (const entry of priorEntries) {
|
||||
fs.copyFileSync(new URL(`${entry.tag}.sql`, migrationsRoot), path.join(priorMigrations, `${entry.tag}.sql`));
|
||||
}
|
||||
const sourceDbClient = createDb(sourceDb.connectionString);
|
||||
await migrate(drizzle(sourceDbClient.$client), { migrationsFolder: priorMigrations });
|
||||
await seedValidWorktreeSource(sourceDb.connectionString);
|
||||
await sourceDbClient.$client.unsafe(`
|
||||
DELETE FROM "drizzle"."__drizzle_migrations"
|
||||
WHERE "id" = (
|
||||
SELECT max("id") FROM "drizzle"."__drizzle_migrations"
|
||||
);
|
||||
|
||||
WITH pair AS (
|
||||
SELECT
|
||||
array_agg("id" ORDER BY "id" DESC) AS ids,
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ interface IssueUpdateOptions extends BaseClientOptions {
|
|||
|
||||
interface IssueCommentOptions extends BaseClientOptions {
|
||||
body: string;
|
||||
attachmentId?: string[];
|
||||
reopen?: boolean;
|
||||
resume?: boolean;
|
||||
}
|
||||
|
|
@ -361,6 +362,10 @@ export function registerIssueCommands(program: Command): void {
|
|||
.description("Add comment to issue")
|
||||
.argument("<issueId>", "Issue ID")
|
||||
.requiredOption("--body <text>", "Comment body")
|
||||
.option(
|
||||
"--attachment-id <id...>",
|
||||
"Bind uploaded issue attachments to this comment",
|
||||
)
|
||||
.option("--reopen", "Reopen if issue is done/cancelled")
|
||||
.option("--resume", "Request explicit follow-up and wake the assignee when resumable")
|
||||
.action(async (issueId: string, opts: IssueCommentOptions) => {
|
||||
|
|
@ -368,6 +373,7 @@ export function registerIssueCommands(program: Command): void {
|
|||
const ctx = resolveCommandContext(opts);
|
||||
const payload = addIssueCommentSchema.parse({
|
||||
body: opts.body,
|
||||
attachmentIds: opts.attachmentId,
|
||||
reopen: opts.reopen,
|
||||
resume: opts.resume,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -84,9 +84,9 @@ export function resolveGitInstallWorkspacePackages(checkoutPath: string): Releas
|
|||
return ordered;
|
||||
}
|
||||
|
||||
function assertSupportedNodeVersion(): void {
|
||||
export function assertSupportedNodeVersion(): void {
|
||||
if (!isSupportedNodeVersion(process.versions.node)) {
|
||||
throw new Error(`Managed installs require Node.js ${MINIMUM_NODE_VERSION} or newer (found ${process.version}).`);
|
||||
throw new Error(`Installing or updating Paperclip requires Node.js ${MINIMUM_NODE_VERSION} or newer (found ${process.version} at ${process.execPath}). Put a supported Node bin directory first on PATH and run 'npx paperclipai@latest install --yes' to re-pin an existing managed install.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { execFile } from "node:child_process";
|
|||
import { promisify } from "node:util";
|
||||
import * as p from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { buildNextManifest, flipCurrentAtomic, isManagedExecutable, pruneInstallPayloads, readInstallManifest, resolveInstallStorePaths, withInstallStoreLock, writeInstallManifestAtomic, type InstallChannel, type InstallManifest, type InstallRecord, type InstallStorePaths } from "../install-store.js";
|
||||
import { assertManagedShimWritable, writeManagedShim, buildNextManifest, flipCurrentAtomic, isManagedExecutable, pruneInstallPayloads, readInstallManifest, resolveInstallStorePaths, withInstallStoreLock, writeInstallManifestAtomic, type InstallChannel, type InstallManifest, type InstallRecord, type InstallStorePaths } from "../install-store.js";
|
||||
import { dbBackupCommand } from "./db-backup.js";
|
||||
import { installGitPayload, installNpmPayload, PUBLIC_NPM_REGISTRY, resolveGitHubRef, resolvePublishedVersion, type CommandRunner } from "./install.js";
|
||||
import { assertSupportedNodeVersion, installGitPayload, installNpmPayload, PUBLIC_NPM_REGISTRY, resolveGitHubRef, resolvePublishedVersion, type CommandRunner } from "./install.js";
|
||||
import { resolvePaperclipInstanceId, resolvePaperclipInstanceRoot } from "../config/home.js";
|
||||
import { resolveConfigPath } from "../config/store.js";
|
||||
import { detectServiceManager } from "../services/service-manager.js";
|
||||
|
|
@ -180,6 +180,7 @@ export async function updateCommand(options: UpdateOptions, overrides: Partial<D
|
|||
}
|
||||
if (mode === "npx") { emit(options, { mode, action: "install" }, "This is an ephemeral npx install. Run `paperclipai install`, then use `paperclipai update` from the managed shim."); return; }
|
||||
if (mode === "source" || mode === "unknown") { emit(options, { mode, action: "manual" }, "This appears to be a source checkout. Update it with `git pull` followed by `pnpm install`; Paperclip will not mutate the repository."); return; }
|
||||
if (!options.check && !options.dryRun) assertSupportedNodeVersion();
|
||||
const request = resolveUpdateRequest(mode === "managed" ? manifest : null, options);
|
||||
if (mode === "managed" && manifest?.source === "git") {
|
||||
if (!manifest.repo || !manifest.ref || !manifest.sha) throw new Error("Managed git install metadata is incomplete.");
|
||||
|
|
@ -193,7 +194,9 @@ export async function updateCommand(options: UpdateOptions, overrides: Partial<D
|
|||
}
|
||||
if (options.backup !== false) await runPreUpdateBackup(options, overrides.backup ?? (() => dbBackupCommand({})), overrides.hasInstanceData);
|
||||
const installed = await withInstallStoreLock(async () => {
|
||||
assertManagedShimWritable(paths);
|
||||
const payload = await installGitPayload(manifest.repo!, targetSha, runCommand, paths);
|
||||
writeManagedShim(paths);
|
||||
const record: InstallRecord = { source: "git", version: payload.version, channel: "pinned", repo: manifest.repo, ref: manifest.ref, sha: targetSha, payloadPath: payload.payloadPath, installedAt: (overrides.now?.() ?? new Date()).toISOString() };
|
||||
const next = buildNextManifest(record, manifest); const oldTarget = fs.readlinkSync(paths.currentPath); flipCurrentAtomic(payload.payloadPath, paths);
|
||||
try { writeInstallManifestAtomic(next, paths); } catch (error) { flipCurrentAtomic(path.resolve(paths.cliRoot, oldTarget), paths); throw error; }
|
||||
|
|
@ -247,7 +250,9 @@ export async function updateCommand(options: UpdateOptions, overrides: Partial<D
|
|||
if (options.dryRun) { emit(options, { mode, currentVersion, targetVersion, action: comparison < 0 ? "downgrade" : "update", backup: options.backup !== false, dryRun: true }, `Would ${comparison < 0 ? "downgrade" : "update"} paperclipai ${currentVersion} → ${targetVersion}${options.backup === false ? " without a backup" : " after a database backup"}.`); return; }
|
||||
if (options.backup !== false) await runPreUpdateBackup(options, overrides.backup ?? (() => dbBackupCommand({})), overrides.hasInstanceData);
|
||||
const installed = await withInstallStoreLock(async () => {
|
||||
assertManagedShimWritable(paths);
|
||||
const payload = await installNpmPayload(targetVersion, runCommand, paths);
|
||||
writeManagedShim(paths);
|
||||
const record: InstallRecord = { source: "npm", version: targetVersion, channel: request.channel, payloadPath: payload.payloadPath, installedAt: (overrides.now?.() ?? new Date()).toISOString() };
|
||||
const next = buildNextManifest(record, manifest); const oldTarget = fs.readlinkSync(paths.currentPath); flipCurrentAtomic(payload.payloadPath, paths);
|
||||
try { writeInstallManifestAtomic(next, paths); } catch (error) { flipCurrentAtomic(path.resolve(paths.cliRoot, oldTarget), paths); throw error; }
|
||||
|
|
|
|||
|
|
@ -380,13 +380,17 @@ function shellQuote(value: string): string {
|
|||
|
||||
function isManagedShimContents(contents: string): boolean {
|
||||
const lines = contents.split("\n");
|
||||
// Accept the original pinned-runtime shim so upgrades can replace it.
|
||||
const withRuntimePath = lines.length === 6;
|
||||
const execIndex = withRuntimePath ? 4 : 3;
|
||||
return (
|
||||
lines.length === 5 &&
|
||||
(lines.length === 5 || withRuntimePath) &&
|
||||
lines[0] === "#!/bin/sh" &&
|
||||
lines[1] === `# ${MANAGED_SHIM_MARKER}` &&
|
||||
lines[2] === "set -eu" &&
|
||||
/^exec '(?:[^']|'"'"')+' '(?:[^']|'"'"')+' "\$@"$/.test(lines[3]) &&
|
||||
lines[4] === ""
|
||||
(!withRuntimePath || /^export PATH='(?:[^']|'"'"')+':"\$\{PATH:-\/usr\/local\/bin:\/usr\/bin:\/bin\}"$/.test(lines[3])) &&
|
||||
/^exec '(?:[^']|'"'"')+' '(?:[^']|'"'"')+' "\$@"$/.test(lines[execIndex]) &&
|
||||
lines[execIndex + 1] === ""
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -399,7 +403,9 @@ export function writeManagedShim(paths = resolveInstallStorePaths()): void {
|
|||
fs.mkdirSync(path.dirname(paths.shimPath), { recursive: true, mode: 0o755 });
|
||||
assertManagedShimWritable(paths);
|
||||
const entrypoint = path.join(paths.currentPath, "node_modules", "paperclipai", "dist", "index.js");
|
||||
const contents = `#!/bin/sh\n# ${MANAGED_SHIM_MARKER}\nset -eu\nexec ${shellQuote(process.execPath)} ${shellQuote(entrypoint)} "\$@"\n`;
|
||||
// ACP servers and package-manager shims use /usr/bin/env node. Pin their
|
||||
// runtime too, even when systemd/launchd supplies a different PATH.
|
||||
const contents = `#!/bin/sh\n# ${MANAGED_SHIM_MARKER}\nset -eu\nexport PATH=${shellQuote(path.dirname(process.execPath))}:"\${PATH:-/usr/local/bin:/usr/bin:/bin}"\nexec ${shellQuote(process.execPath)} ${shellQuote(entrypoint)} "\$@"\n`;
|
||||
writeFileAtomic(paths.shimPath, contents, 0o755);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ describe("isSupportedNodeVersion", () => {
|
|||
expect(warning).toContain(NODE_VERSION_INSTALL_GUIDE_URL);
|
||||
expect(warning).toContain("piped install.sh form cannot upgrade");
|
||||
expect(warning).toContain("Restart Paperclip after upgrading");
|
||||
expect(warning).toContain(process.execPath);
|
||||
expect(warning).toContain("startup executable and PATH");
|
||||
});
|
||||
|
||||
it("emits at most one warning when CLI and server boot in the same process", () => {
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@
|
|||
"outDir": "dist",
|
||||
"rootDir": ".."
|
||||
},
|
||||
"include": ["src", "../packages/shared/src", "../packages/plugins/create-paperclip-plugin/src"]
|
||||
"include": ["src", "../packages/shared/src", "../server/src/types/express.d.ts", "../packages/plugins/create-paperclip-plugin/src"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,6 +79,11 @@ When a task produces a user-inspectable deliverable file:
|
|||
4. Link the printed attachment URL in the final issue comment.
|
||||
5. Then set the final issue status.
|
||||
|
||||
For a response that is explicitly intended for an external chat conversation,
|
||||
also pass each intended file with `paperclipai issue comment --attachment-id
|
||||
<id>`. Paperclip binds only those exact uploaded files to that comment; other
|
||||
task attachments remain internal.
|
||||
|
||||
Final comments should name and link the uploaded artifact or work product, not
|
||||
just the local filesystem path. For workspace-only files, include the work
|
||||
product title and recorded relative path. Local paths can be included as
|
||||
|
|
|
|||
|
|
@ -427,7 +427,7 @@ npx paperclipai issue get <issue-id-or-identifier>
|
|||
npx paperclipai issue create --company-id <company-id> --title "..." [--description "..."] [--status todo] [--priority high]
|
||||
npx paperclipai issue update <issue-id> [--status in_progress] [--comment "..."]
|
||||
npx paperclipai issue delete <issue-id> --yes
|
||||
npx paperclipai issue comment <issue-id> --body "..." [--reopen]
|
||||
npx paperclipai issue comment <issue-id> --body "..." [--attachment-id <id...>] [--reopen]
|
||||
npx paperclipai issue comments <issue-id> [--limit 50]
|
||||
npx paperclipai issue comment:get <issue-id> <comment-id>
|
||||
npx paperclipai issue comment:delete <issue-id> <comment-id>
|
||||
|
|
|
|||
|
|
@ -122,8 +122,10 @@ All of these are optional; when unset, the driver defaults apply and behavior is
|
|||
```sh
|
||||
DATABASE_PREPARED_STATEMENTS=false # required for transaction-mode poolers; default: enabled
|
||||
DATABASE_POOL_MAX=25 # connection pool size; default: 10
|
||||
DATABASE_IDLE_TIMEOUT_SECONDS=60 # close idle pooled connections; default: keep open
|
||||
DATABASE_IDLE_TIMEOUT_SECONDS=60 # close idle pooled connections; default: 60 (0 = keep open)
|
||||
DATABASE_CONNECT_TIMEOUT_SECONDS=10 # default: 30
|
||||
DATABASE_MAX_LIFETIME_SECONDS=1800 # recycle a pooled connection after this long; default: 30-60 min (random)
|
||||
DATABASE_APPLICATION_NAME=paperclip # application_name in pg_stat_activity; default: paperclip
|
||||
```
|
||||
|
||||
### Push the schema
|
||||
|
|
@ -245,6 +247,15 @@ finalization ledger, whose retry time and owner lease are checked under a row
|
|||
lock. None of these writes selects a runtime or changes a legacy run's execution
|
||||
path.
|
||||
|
||||
Durable agent session goals are an additive projection on
|
||||
`agent_task_sessions`, distinct from the business-goal hierarchy. The row stores
|
||||
the negotiated goal capability, normalized snapshot and status, desired state,
|
||||
provider source cursor, monotonic projection revision, and observation time.
|
||||
`agent_session_goal_actions` is the control outbox: `(session_id, request_id)`
|
||||
is unique, so retries return the original accepted action. Provider source
|
||||
ordering fences duplicate and stale updates, and a cleared projection retains
|
||||
its revision/cursor tombstone so an older provider event cannot resurrect it.
|
||||
|
||||
Issue `status_version` advances only when `status` changes. The JavaScript backup
|
||||
path includes user-defined functions and triggers so a restored database keeps
|
||||
that invariant. Removing or disabling a future native rollout flag must not
|
||||
|
|
@ -260,6 +271,35 @@ and process-start evidence proves the prior controller is gone, or when the
|
|||
lease expires. Recovery generation changes do not increment the independent
|
||||
provider-attempt counter.
|
||||
|
||||
## Telegram private draft identities
|
||||
|
||||
`chat_telegram_draft_ids` is a content-free, instance-wide PostgreSQL sequence,
|
||||
not a company-owned record. Telegram's native Stop callback carries a draft ID
|
||||
but no actor or Paperclip generation. IDs therefore must not be recycled when
|
||||
a transaction rolls back or an endpoint/company is deleted and its bot is
|
||||
connected again. The sequence allocates positive 31-bit IDs without cycling;
|
||||
exhaustion refuses new draft allocation rather than wrapping or falling back to
|
||||
random IDs. Never reset it as part of chat cleanup.
|
||||
|
||||
The matching `chat_actions` entry remains company/endpoint-scoped and binds the
|
||||
draft to its exact conversation, publication attempt, runtime, credential and
|
||||
approved text. Stop can suppress that private draft's final publication; it
|
||||
cannot cancel a task or model run. Logical backups preserve the sequence, but
|
||||
restoring an older database may roll back its high-water mark: disaster recovery
|
||||
must not assume stale provider Stop events are safe to reuse. That restore
|
||||
boundary is not qualified by the rollback/concurrency regression.
|
||||
|
||||
## Attachment upload provenance
|
||||
|
||||
`issue_attachments.originating_run_id` records server-derived run attribution at
|
||||
upload time. It is not writable through attachment or work-product update APIs.
|
||||
Legacy attachments and uploads without a registered run keep a null value; the
|
||||
migration deliberately does not infer attribution from mutable work products.
|
||||
Deleting the originating run clears the reference and fails closed for automatic
|
||||
chat handoff. An agent's external file selection must match the attachment's
|
||||
company, task, agent, and originating run. Editing or recreating a work-product
|
||||
record cannot reassign that authority to a later run.
|
||||
|
||||
## Question-response delivery receipts
|
||||
|
||||
`issue_question_response_deliveries` is the retry-safe, content-free outbox for
|
||||
|
|
|
|||
|
|
@ -82,6 +82,12 @@ pnpm build-storybook
|
|||
|
||||
These run the `@paperclipai/ui` Storybook on port `6006` and build the static output to `ui/storybook-static/`.
|
||||
|
||||
Use **Chat & Comments → Issue Thread Interactions → Composer Questions Auto Advance**
|
||||
to try the paged composer form. A single selection shows a brief checked-state animation before advancing to the
|
||||
next question. Reduced-motion mode advances without animation.
|
||||
Multi-select and custom answers wait for Next, and the final page waits for
|
||||
Submit answers. The adjacent **Verified** story exercises the full flow.
|
||||
|
||||
The Storybook visual regression suite uses external PNG baselines instead of
|
||||
committed screenshots:
|
||||
|
||||
|
|
@ -110,6 +116,70 @@ workflow manually, to produce downloadable Playwright report/test-result
|
|||
artifacts. Normal PR visual runs use read-only repository permissions and do not
|
||||
upload or mutate baseline objects.
|
||||
|
||||
### Publish a branch Storybook
|
||||
|
||||
CODEOWNERS can publish a repository branch through **Actions → Storybook Deploy →
|
||||
Run workflow**. Keep the workflow branch on `master` and enter the source branch
|
||||
in `branch`. The source branch does not need to contain the workflow. Leaving
|
||||
`branch` empty publishes the selected workflow branch's dispatched commit.
|
||||
|
||||
```sh
|
||||
gh workflow run storybook-deploy.yml --ref master -f branch=your-branch
|
||||
```
|
||||
|
||||
The existing **Storybook Visual** workflow also offers a `deploy_preview` checkbox,
|
||||
which publishes through the same workflow instead of running visual tests:
|
||||
|
||||
```sh
|
||||
gh workflow run storybook-visual.yml --ref master -f deploy_preview=true -f branch=your-branch
|
||||
```
|
||||
|
||||
Approve the `storybook-deploy` environment as a CODEOWNER. The workflow summary
|
||||
links the **stable branch URL** and **this build**. The run also uploads a
|
||||
`storybook-deployment-<run-id>-<attempt>` artifact containing
|
||||
`storybook-deployment.md` with both links and the source commit. Different branches have
|
||||
different URLs; publishing one never replaces another. Redeploying the same
|
||||
branch updates its stable URL only after all files for the new build are uploaded.
|
||||
Previous build links keep working. The branch entry preserves Storybook query
|
||||
parameters and fragments when redirecting to the completed build.
|
||||
|
||||
Bookmark URLs use `storybook/branches/<branch>/`, for example
|
||||
`https://d1p6rlowie26tp.cloudfront.net/storybook/branches/master/`.
|
||||
Copy the **stable branch URL** from the run summary when saving a bookmark;
|
||||
opening it redirects to the latest published build. Branch names preserve case.
|
||||
Characters other than letters, digits, `_`, and `-` use `~HH` UTF-8 escapes, so
|
||||
`feature/foo` becomes `feature~2Ffoo` and stays distinct from `feature-foo`.
|
||||
Names ending in a hyphen and 16 lowercase hex digits escape that hyphen to
|
||||
reserve the existing build directories. Very long names use a hash suffix.
|
||||
Existing hashed branch URLs keep updating and remain valid. Build files remain
|
||||
under `storybook/branches/<readable-branch>-<hash>/builds/<run-id>-<attempt>/`.
|
||||
`deployment.json` in each build records its branch, source commit and URLs.
|
||||
Builds run independently; publication is serialized per branch. Retained builds
|
||||
are not automatically deleted and will accumulate until an operator prunes them.
|
||||
|
||||
Publishing requires both the original actor and the current rerunner to be
|
||||
individual GitHub accounts named in `.github/CODEOWNERS` on the current default
|
||||
branch. Comments, teams and email entries do not grant access. Authorization runs
|
||||
before the build and again before deployment, including deployment-only reruns.
|
||||
GitHub also requires a CODEOWNER environment approval, so editing authorization
|
||||
code on a branch cannot grant AWS access without an authorized reviewer.
|
||||
|
||||
The build downloads the public source archive with no GitHub token permissions,
|
||||
AWS credentials or repository secrets. Dependency caching and install lifecycle
|
||||
scripts are disabled. The separate publisher uses GitHub OIDC to assume a role limited to
|
||||
`storybook/branches/*`. It treats the build artifact as static files and runs only
|
||||
the publisher from the workflow checkout. It cannot delete objects, change AWS
|
||||
settings, or overwrite the runner dashboard. The Storybook site itself is public.
|
||||
Pushes and PR events never publish it.
|
||||
|
||||
The existing S3 bucket and CloudFront distribution also serve runner reports in
|
||||
separate prefixes. GitHub Pages and its dashboard workflow are independent.
|
||||
See [Storybook deployment setup](STORYBOOK-DEPLOYMENT.md) for the environment,
|
||||
repository variables, AWS policies and one-time operator setup.
|
||||
|
||||
GitHub requires a new dispatch workflow to exist on the default branch before
|
||||
it becomes a manual entry point.
|
||||
|
||||
## UI Fonts And Screenshots
|
||||
|
||||
The board UI ships its own sans-serif webfont assets in `ui/public/fonts/`.
|
||||
|
|
@ -131,6 +201,13 @@ pnpm dev:stop
|
|||
|
||||
`pnpm dev:once` now tracks backend-relevant file changes and pending migrations. When the current boot is stale, the board UI shows a `Restart required` banner. You can also enable guarded auto-restart in `Instance Settings > Experimental`, which waits for queued/running local agent runs to finish before restarting the dev server.
|
||||
|
||||
Worktree dependency provisioning records its fingerprint only after a successful
|
||||
install. Frozen installs with outdated lockfiles or patched-dependency hash
|
||||
mismatches retry once without `--frozen-lockfile`; other failures retain their
|
||||
exit status. Patch contents are part of the install fingerprint. Generated
|
||||
lockfile changes remain local to the worktree; the repository's lockfile bot
|
||||
owns committed updates.
|
||||
|
||||
## Hot-Restart Deploys
|
||||
|
||||
Primary-instance rebuilds that restart `paperclip.service` can request one-shot live-run adoption instead of using the normal graceful shutdown drain. Before restarting the service, write the marker from the newly staged app with the current service PID:
|
||||
|
|
@ -153,7 +230,7 @@ at least one identity source. Supported-platform process probes fail explicitly
|
|||
instead of silently treating a live PID as either the original owner or a
|
||||
recycled process when identity cannot be established.
|
||||
|
||||
Use `--drain-required` only when the deploy intentionally requires the old terminate-and-retry behavior. Without that flag, the old server verifies that the marker targets its own PID, stops new scheduler work, waits for any queue-claim callback already in flight, snapshots currently running heartbeat run IDs and child PIDs, and skips the shutdown drain so eligible detached local-agent processes can keep running. ACP-backed local runs use server-owned stdio and cannot survive their parent server, so the old server instead persists their complete snapshot, changes the marker to `drainRequired` with `drainReason: "active_acp_run"`, and drains only those runs to queued retries. Detached CLI runs remain eligible for adoption during the same mixed restart. If an ACP process terminates but its terminal run update does not persist, startup classifies it as lost with reason `selective_drain_not_finalized` rather than treating the drain as successful. On startup the new server writes `$PAPERCLIP_HOME/instances/${PAPERCLIP_INSTANCE_ID:-default}/hot-restart-report.json` with `previousServerPid`, `newServerPid`, `previousServerVersion`, `newServerVersion`, `drainReason`, `adoptedRunIds`, `finalizedWhileDownRunIds`, `lostRunIds`, and per-run classifications before the normal orphan reaper runs.
|
||||
Use `--drain-required` only when the deploy intentionally requires the old terminate-and-retry behavior. Without that flag, the old server verifies that the marker targets its own PID, stops new scheduler work, waits for any queue-claim callback already in flight, snapshots currently running heartbeat run IDs and child PIDs, and skips the shutdown drain so eligible detached local-agent processes can keep running. ACP-backed local runs use server-owned stdio and cannot survive their parent server, so the old server instead persists their complete snapshot, changes the marker to `drainRequired` with `drainReason: "active_acp_run"`, and drains only those runs to bounded conversation retries. These retries resume the prior session when compatible, otherwise carry the full task conversation into a fresh session. They do not automatically replay tool calls or require receipts for every prior action. Detached CLI runs remain eligible for adoption during the same mixed restart. If an ACP process terminates but its terminal run update does not persist, startup classifies it as lost with reason `selective_drain_not_finalized` rather than treating the drain as successful. On startup the new server writes `$PAPERCLIP_HOME/instances/${PAPERCLIP_INSTANCE_ID:-default}/hot-restart-report.json` with `previousServerPid`, `newServerPid`, `previousServerVersion`, `newServerVersion`, `drainReason`, `adoptedRunIds`, `finalizedWhileDownRunIds`, `lostRunIds`, and per-run classifications before the normal orphan reaper runs.
|
||||
|
||||
When Paperclip manages embedded PostgreSQL, it suppresses that dependency's eager
|
||||
`SIGINT`/`SIGTERM` cleanup hooks. Paperclip owns signal ordering so the heartbeat
|
||||
|
|
@ -800,12 +877,32 @@ When a workspace service runs Paperclip for browser OAuth QA, configure its `exp
|
|||
|
||||
## Paperclip Runner Adapter Conversion
|
||||
|
||||
The experimental Paperclip Runner currently qualifies four local profiles:
|
||||
Codex, OpenCode, ACPX Claude, and ACPX Codex. Changing an existing agent to
|
||||
`paperclip_runner` remains supported only from `codex_local`; create the other
|
||||
profiles explicitly after enabling the single **Paperclip Runner** experimental
|
||||
setting. Onboarding continues to create legacy adapters. Disabling the setting
|
||||
blocks fresh native starts without hiding or corrupting persisted native runs.
|
||||
The experimental Paperclip Runner offers native Codex, OpenCode, and **ACPX
|
||||
Claude**. Converting an existing Claude, Codex, or OpenCode agent selects its
|
||||
corresponding provider, preserves compatible models, credentials, workspace,
|
||||
and instructions, and resets execution sessions while retaining run history.
|
||||
Other adapters require an explicit provider choice. Legacy ACPX Codex agent
|
||||
settings normalize to native Codex on configuration updates and before fresh
|
||||
runs; immutable run descriptors remain readable. The **Paperclip Runner**
|
||||
experimental setting and company access checks still apply.
|
||||
|
||||
Agent configuration uses the same section layout across adapters: model and
|
||||
provider belong to **Adapter**, environment variables have their own section,
|
||||
and command/extra arguments are folded under **Configuration → Advanced**.
|
||||
Lifecycle, timeout, and interrupt grace settings live under **Advanced Run
|
||||
Policy**. Permission selectors with a single valid mode are hidden; a saved
|
||||
unsupported mode still exposes remediation.
|
||||
|
||||
Model catalogs and refresh follow the selected provider. ACPX Claude uses the
|
||||
normal Claude catalog and accepts custom model IDs; the exact ID is sent to
|
||||
Claude, which can reject unavailable models. Package/version verification is
|
||||
independent of model selection. Environment tests verify runtime installation;
|
||||
a successful provider run additionally verifies credentials and model access.
|
||||
|
||||
ACPX Claude supports Linux x64 and macOS ARM64/x64 with pinned SDK executables.
|
||||
On macOS the launcher uses private verified module/executable snapshots instead
|
||||
of Linux `/proc` descriptors. Dependency isolation, process ownership, and
|
||||
cancellation remain enforced; the snapshot is removed when the provider exits.
|
||||
|
||||
Native Codex is qualified only with `codexPermissionMode: "never"`. The create
|
||||
and edit surfaces do not offer `on-request` or `untrusted`, and a persisted
|
||||
|
|
@ -864,6 +961,14 @@ that classification finishes.
|
|||
signal it or spawn a replacement.
|
||||
- A persisted proposed or terminal result is reconciled before any runner or
|
||||
provider work starts, so restart recovery cannot submit a duplicate turn.
|
||||
- On the next run, a completed local Codex session whose warm controller died
|
||||
before suspension is recovered automatically, including a uniquely verified
|
||||
checkpoint quarantined by older controllers. Paperclip requires matching
|
||||
database/session/provider identities, a settled terminal journal, no pending
|
||||
commands or active provider turn, and a confirmed-dead process and process
|
||||
group. It seals the old authority for normal epoch rotation and preserves the
|
||||
Codex thread and goal state. Empty retry directories do not prevent recovery;
|
||||
conflicting histories, changed profiles, and live or unverifiable owners do.
|
||||
|
||||
Run the credential-free real-process restart suite with:
|
||||
|
||||
|
|
@ -1279,3 +1384,35 @@ Networking behavior for this smoke script:
|
|||
### GitHub identity for shared agents
|
||||
|
||||
See [execution GitHub identity](execution-github-identity.md) for the operation-time credential contract, continuation rules, runtime rollout, and acceptance-test requirements.
|
||||
|
||||
|
||||
### Investigating polling load
|
||||
|
||||
The company heartbeat-run and live-run lists load secret registries in one
|
||||
company-scoped query per response. Registry reads project only
|
||||
`paperclipSecretRedactions` from the run context. They do not load the full
|
||||
prompt/context JSON. Decrypted values live only for that request and each run
|
||||
uses its own registry.
|
||||
|
||||
Hidden browser tabs suspend the company live-events connection and transcript
|
||||
log reads. Returning to a visible tab refreshes active queries once and resumes
|
||||
transcript reads from their retained offsets. A queued live-event invalidation
|
||||
that flushes after the tab hides marks data stale without starting a refetch.
|
||||
The developer-server health poll also stops in hidden tabs.
|
||||
|
||||
Workspace detail responses share concurrent Git inspections and reuse their
|
||||
results for up to five seconds after completion. The cache holds at most 256
|
||||
entries. Close-readiness checks, the terminal-workspace reaper, and the final
|
||||
cleanup validation still inspect Git afresh. A display result never authorizes
|
||||
worktree removal.
|
||||
|
||||
The connection-health sweep selects only due IDs in SQL before applying its
|
||||
limit. Legacy `paperclip_plugin` placeholder connections are excluded: their
|
||||
tools run in plugin workers and do not have remote MCP endpoints. These rows
|
||||
remain available; the sweep does not disable or delete plugin connections.
|
||||
|
||||
When investigating an overloaded instance, distinguish request amplification
|
||||
from stored configuration problems. Verify connection transport and endpoint
|
||||
fields before disabling a connection. Verify workspace ownership, active runs,
|
||||
Git state, and runtime-service readiness before closing a workspace. A missing
|
||||
URL or old workspace timestamp alone does not prove that a row is disposable.
|
||||
|
|
|
|||
|
|
@ -18,12 +18,55 @@ Build arguments:
|
|||
|-----|---------|---------|
|
||||
| `USER_UID` | `1000` | UID for the container `node` user (match your host UID to avoid permission issues on bind mounts) |
|
||||
| `USER_GID` | `1000` | GID for the container `node` group |
|
||||
| `CLI_TOOLS_CACHE_EPOCH` | empty | Refresh the CLI-install layer; CI supplies the current ISO week |
|
||||
| `PAPERCLIP_BUILD_VERSION` | empty | Runtime version when Git metadata is unavailable |
|
||||
| `PAPERCLIP_BUILD_COMMIT` | empty | Source commit written into the server build stamp and runtime environment |
|
||||
|
||||
Changing the build version or commit preserves the CLI-install cache. The
|
||||
tool layer refreshes when its weekly epoch, base image, installation command,
|
||||
or earlier build inputs change. Local builds can set a new epoch explicitly
|
||||
to refresh tools without clearing the entire build cache.
|
||||
|
||||
```sh
|
||||
docker build -t paperclip-local \
|
||||
--build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g) .
|
||||
```
|
||||
|
||||
## Cloud image addresses
|
||||
|
||||
The Docker workflow publishes the managed deployment image for Linux AMD64.
|
||||
`Cloud readiness` starts `Docker cloud` on each master push independently of the
|
||||
multi-platform self-hosted build. Different commits use separate concurrency groups and existing
|
||||
GitHub-hosted runners, so an older production or cloud build does not hold the
|
||||
new commit in a workflow queue. Available GitHub runner capacity still applies.
|
||||
Release tags and manual `Docker` dispatches call the same cloud build workflow.
|
||||
|
||||
Each commit exports to its own `buildcache-cloud-<FULL_SHA>` registry tag.
|
||||
Builds import the current commit and nine first-parent ancestors, plus the
|
||||
legacy `buildcache-cloud` fallback. This preserves reusable layers without
|
||||
letting concurrent builds overwrite one shared cache manifest. Retain recent
|
||||
cache tags if registry cleanup is configured; deleting them makes builds colder.
|
||||
|
||||
Cloud CI skips SDK and cache cleanup when both the Docker data filesystem and
|
||||
the checkout filesystem have at least 64 GiB available. Below that conservative
|
||||
headroom threshold, or when the measurement fails, it retains the existing
|
||||
cleanup. The threshold selects the fast path; it is not a new minimum disk
|
||||
requirement for local builds or smaller runners.
|
||||
|
||||
After the pushed image passes its Sentry and orphan-reaping checks, the workflow verifies its
|
||||
commit label and platform and adds `ghcr.io/paperclipai/paperclip:sha-<full-commit-sha>-cloud`.
|
||||
This address lets commit-based deployment tooling reuse the normal build.
|
||||
Existing short-SHA and release tags remain available.
|
||||
|
||||
The full-SHA tag identifies the source commit. It does not certify that source
|
||||
tests passed or that a compatible database migrator is available. Deployment
|
||||
tooling must still check those prerequisites and pin the resolved image digest;
|
||||
a rebuild of the same source can update the tag's digest.
|
||||
|
||||
The separate [cloud readiness check](cloud-build-readiness.md) combines source
|
||||
verification, successful cloud image checks, and exact-source migrator
|
||||
availability. It runs outside the full npm release's concurrency queue.
|
||||
|
||||
## One-liner (build + run)
|
||||
|
||||
```sh
|
||||
|
|
@ -295,3 +338,25 @@ Notes:
|
|||
|
||||
- The `docker-entrypoint.sh` adjusts the container `node` user UID/GID at startup to match the values passed via `USER_UID`/`USER_GID`, avoiding permission issues on bind-mounted volumes.
|
||||
- Paperclip data persists via Docker volumes/bind mounts (compose) or at `~/.local/share/paperclip` (quadlet).
|
||||
|
||||
## Native Runner build cache
|
||||
|
||||
The image compiles the native Runner in `runner-build`, before copying the
|
||||
application source. That stage includes the pinned Rust compiler, the complete
|
||||
Cargo workspace and lockfile, and the protocol schemas and fixtures embedded
|
||||
by Rust. Changes to those inputs rebuild the native binary. Ordinary server or
|
||||
UI changes can reuse it through the existing registry cache (`mode=max`). Each
|
||||
platform gets its own native build; no cross-architecture binary is reused.
|
||||
|
||||
The application build inherits that stage and still runs the normal server
|
||||
build, including Cargo, binary staging, and generated-contract checks. Rust
|
||||
input file times are normalized in both stages so fresh checkouts do not force
|
||||
Cargo to rebuild unchanged source. Changes made by build scripts still reach
|
||||
Cargo's normal validation. The final application copy excludes Cargo's target
|
||||
directory as before. Cache misses only cost compilation time.
|
||||
|
||||
Pull requests that change the Dockerfile, Docker ignore rules, or Runner native
|
||||
inputs also build the isolated `runner-build` target in `Docker Runner check`.
|
||||
This compiles against the actual reduced context and catches missing embedded
|
||||
inputs before the post-merge image build. It uses a GitHub-hosted runner with
|
||||
read-only repository access and does not publish images or cache artifacts.
|
||||
|
|
|
|||
|
|
@ -61,6 +61,45 @@ the same origin as the artifact as an independent trust anchor.
|
|||
Each installer flag also has a `PAPERCLIP_INSTALL_*` environment-variable
|
||||
equivalent. This helps where passing arguments through a pipe is awkward.
|
||||
|
||||
Codex ACP workspace sessions enable networking so agents can report task outcomes.
|
||||
To disable it explicitly, set `extraArgs` to
|
||||
`["-c", "sandbox_workspace_write.network_access=false"]`, or set
|
||||
`env.PAPERCLIP_CODEX_ACP_NETWORK_ACCESS="false"`. Execution-target network denial
|
||||
also remains enforced. Read-only ACP mode remains read-only.
|
||||
|
||||
## Node runtime used by background services
|
||||
|
||||
Check the Node executable used by the running service, not only `node --version`
|
||||
in an interactive shell. Systemd and launchd do not load shell version-manager
|
||||
configuration. A newer Node installed elsewhere does not upgrade a running
|
||||
service or change a custom startup script's `PATH`.
|
||||
|
||||
Managed installs pin the validated Node executable in the `paperclipai` shim
|
||||
and prepend its directory to `PATH` for child tools, including ACP servers with
|
||||
an `/usr/bin/env node` shebang. Re-run the installer using the supported
|
||||
Node runtime after changing runtime installations, then restart the service.
|
||||
For example, put the supported Node's bin directory first on `PATH` and run
|
||||
`npx paperclipai@latest install --yes`. Do not use the old managed shim to
|
||||
re-pin Node: it intentionally continues launching its previously pinned runtime.
|
||||
Installs and updates refresh existing managed shims in place. Updates reject an
|
||||
unsupported running Node before installing or activating a payload; read-only
|
||||
update checks and rollback remain available for recovery. Global npm installs and
|
||||
source checkout services must configure their own executable and child-process `PATH`.
|
||||
|
||||
For custom service wrappers, use an absolute, supported Node executable and put
|
||||
that executable's directory first on `PATH`. Keep required existing PATH entries.
|
||||
On Linux, verify the running executable with `/proc/<server-pid>/exe`; an
|
||||
interactive shell version check alone is insufficient. Use the guarded restart
|
||||
procedure in [DEVELOPING.md](DEVELOPING.md#hot-restart-deploys) when jobs are active.
|
||||
|
||||
Legacy local adapters default to ACP, including configurations with no `engine`
|
||||
field or the old `auto` value. An unavailable ACP runtime fails the run and the
|
||||
agent environment test with a setup error; it never silently changes engines.
|
||||
Repair the reported prerequisite or explicitly select `engine: cli`. Local
|
||||
filesystem/network confinement and in-place Codex workspaces require explicit
|
||||
CLI selection. CLI sandbox defaults and explicit restrictions are described in
|
||||
the adapter configuration documentation.
|
||||
|
||||
## Managed Install Layout
|
||||
|
||||
Managed code is separate from instance data:
|
||||
|
|
|
|||
|
|
@ -335,3 +335,59 @@ Check:
|
|||
- [doc/RELEASING.md](RELEASING.md)
|
||||
- [doc/PUBLISHING.md](PUBLISHING.md)
|
||||
- [doc/plans/2026-03-17-release-automation-and-versioning.md](plans/2026-03-17-release-automation-and-versioning.md)
|
||||
|
||||
## Runner verification dependency cache
|
||||
|
||||
`release-verify.yml` caches Cargo dependencies for its `Verify Paperclip Runner`
|
||||
job using a pinned Rust Cache action. It selects the compiler from the Runner
|
||||
package's `rust-toolchain.toml` before computing the cache key. Compiler and Cargo
|
||||
metadata changes select a new cache; the `release-runner-v1` shared key lets
|
||||
callers of this reusable verification workflow reuse the same dependency cache.
|
||||
|
||||
Workspace crates and installed Cargo binaries are excluded. Every run still
|
||||
builds the Runner workspace and runs `check:all`, including the Rust and
|
||||
TypeScript tests. Only an own-repository master-push run verifying that push's exact
|
||||
SHA can restore the cache, and only a successful run saves it. PR, tag, and
|
||||
manual candidate verification compile without this cache. A miss or eviction costs compilation time but does not change the checks.
|
||||
To discard old dependency caches, increment the shared-key version and let the
|
||||
next successful master verification warm it again.
|
||||
|
||||
The trust boundary is the protected master branch, not the cache-key text.
|
||||
GitHub does not let master restore caches created by a child branch, sibling
|
||||
branch, tag, or PR merge ref. Both permitted restore scopes (current branch and
|
||||
default branch) are master here. A workflow with authority to execute arbitrary
|
||||
code on master can affect verification directly and is already trusted. The
|
||||
cache contains dependency build artifacts, not credentials or workspace output.
|
||||
See [GitHub cache access restrictions](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache).
|
||||
|
||||
## Chat integration test shards
|
||||
|
||||
Release verification runs the large chat integration file on three independent
|
||||
runners. Five other server shards cover every remaining general server file.
|
||||
The ordinary local test command and trusted PR workflow keep their complete
|
||||
`general-server` group. Each chat case shuts down its services, pauses its own
|
||||
still-active endpoints, and retires its active/waiting conversations after
|
||||
assertions. This keeps workers in later cases from claiming earlier
|
||||
fixtures in the shared test database. Application assertions stay unchanged.
|
||||
|
||||
Each chat job collects active tests with Vitest, groups cases by source line,
|
||||
and balances those groups by case count. Parameterized cases and loop-generated
|
||||
cases on one line stay together. The job re-collects with the exact line filters
|
||||
it will execute and fails if the selected case identities differ. Hooks and test
|
||||
execution remain sequential inside each runner with its own temporary home.
|
||||
|
||||
Run one shard locally with:
|
||||
|
||||
```sh
|
||||
pnpm test:run:general -- --group general-chat --shard-index 0 --shard-count 3
|
||||
```
|
||||
|
||||
Use indexes 0, 1, and 2 to run the complete chat suite. The CLI validates that
|
||||
each shard has work and that collection includes usable source locations. A
|
||||
Vitest collection or filtering change fails verification instead of dropping
|
||||
tests. Splitting adds three release-verification jobs and repeats collection and
|
||||
fixture setup; it does not make a single test faster.
|
||||
|
||||
The file-duration manifest also records the native Codex Runner integration
|
||||
suite's measured import and execution cost, so the existing file balancer
|
||||
accounts for it in both ordinary PR and release verification.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ Paperclip V1 must provide a full control-plane loop for autonomous agents:
|
|||
4. All work is tracked through tasks/comments with audit visibility.
|
||||
5. Token/cost usage is reported and budget limits can stop work.
|
||||
6. The board can intervene anywhere (pause agents/tasks, override decisions).
|
||||
An effective task or ancestor pause replaces the message composer with an
|
||||
amber Resume takeover. New board messages, including updates with comments,
|
||||
are rejected until the hold is released. Drafts survive pause and resume.
|
||||
|
||||
Success means one operator can run a small AI-native company end-to-end with clear visibility and control.
|
||||
|
||||
|
|
@ -38,7 +41,7 @@ These decisions close open questions from `SPEC.md` for V1.
|
|||
| Communication | Tasks + comments only (no separate chat system) |
|
||||
| Task ownership | Single assignee; atomic checkout required for `in_progress` transition |
|
||||
| Task watchdogs | A task watchdog is an explicitly configured, issue-subtree-scoped verification and recovery capacity. It may restore live task paths inside the watched subtree; for issue-thread interaction resolution it is an ordinary agent subject to the same audience and containment checks, not board authority, active-run output monitoring, or general liveness recovery. |
|
||||
| Recovery | Liveness/watchdog recovery preserves explicit ownership: retry lost execution continuity where safe, otherwise open visible source-scoped recovery actions by default, use issue-backed recovery only for independent repair work, or require human escalation (see `doc/execution-semantics.md`) |
|
||||
| Recovery | Liveness/watchdog recovery preserves explicit ownership: continue interrupted local conversations with bounded fresh turns and preserved history, never replay tool calls automatically; retain native ownership and real execution gates; otherwise open visible source-scoped recovery actions by default, use issue-backed recovery only for independent repair work, or require human escalation (see `doc/execution-semantics.md`) |
|
||||
| Agent adapters | Built-in `process`, `http`, local CLI/session adapters, and OpenClaw gateway support; external adapters can also be loaded through the adapter plugin flow |
|
||||
| Plugin framework | Local/self-hosted early plugin runtime is in scope; cloud marketplace and packaged public distribution remain out of scope |
|
||||
| Auth | Mode-dependent human auth (`local_trusted` implicit board in current code; authenticated mode uses sessions), API keys for agents |
|
||||
|
|
@ -222,6 +225,14 @@ Invariant:
|
|||
|
||||
Routine execution issues add a routine-scoped env overlay after project env and before Paperclip runtime-owned keys. Routine env uses the same secret-aware binding format, is stored on `routines.env`, is snapshotted in routine revisions, and resolves secret refs against the routine binding target so routine-owned secrets do not require direct bindings on the executing agent.
|
||||
|
||||
Project source repositories use the existing `project_workspaces` collection.
|
||||
Each selected GitHub repository has a canonical `repo_url` and stable provider ID
|
||||
in `metadata.githubRepositoryId`; the first workspace remains the execution default.
|
||||
The board can select multiple repositories from its usable personal and shared
|
||||
GitHub grants. Selection does not grant runtime credential access. Legacy workspace
|
||||
URLs remain valid. Project creation and repository replacement are transactional.
|
||||
See `doc/project-repositories.md` for the API and UI contract.
|
||||
|
||||
## 7.6 `issues` (core task entity)
|
||||
|
||||
- `id` uuid pk
|
||||
|
|
@ -242,6 +253,9 @@ Routine execution issues add a routine-scoped env overlay after project env and
|
|||
- `created_by_user_id` uuid fk `users.id` null
|
||||
- identifier fields: `issue_number`, `identifier`
|
||||
- origin fields: `origin_kind`, `origin_id`, `origin_run_id`, `origin_fingerprint`
|
||||
- Creation stores the actor run in `origin_run_id` unless an explicit origin run is supplied. `GET /api/companies/:companyId/issues?createdFromIssueId=<uuid>` selects tasks created by runs bound to that source task, using native run issue identity or persisted legacy task context. Historical rows without an origin run may use their recorded creation activity; comments and shared creators do not establish provenance. Source, run, activity and result are company-scoped.
|
||||
- Relation lists can use `sortField=id&sortDir=asc&afterId=<uuid>` for stable pagination. The cursor excludes earlier IDs and cannot be combined with an offset or activity-based order.
|
||||
- The streamlined task page's Tasks tab keeps two independent memberships: the existing subtask tree, and created tasks grouped by their current project (or No project). A created subtask appears in both. Only Subtasks has completion progress; groups collapse independently and unfinished tasks sort above finished tasks.
|
||||
- `request_depth` int not null default 0
|
||||
- `work_mode` text not null default `standard`; supported values:
|
||||
- `standard`: normal autonomous execution. Agents may investigate, edit files, create artifacts, and complete the task.
|
||||
|
|
@ -1037,6 +1051,17 @@ instances return `404`.
|
|||
- `GET /issues/:issueId/attachments`
|
||||
- `GET /attachments/:attachmentId/content`
|
||||
- `DELETE /attachments/:attachmentId`
|
||||
- `GET /issues/:issueId/runner-goal?agentId=...`
|
||||
- `POST /issues/:issueId/runner-goal/actions`
|
||||
|
||||
The runner-goal endpoints control an issue-scoped durable agent-session goal,
|
||||
not a row in the company `goals` hierarchy. Reads return the effective agent,
|
||||
negotiated capability, normalized goal snapshot, active-run state, pending
|
||||
action, and revision. Mutations require a request id, assigned agent, expected
|
||||
revision, and a negotiated action; they return `202`, replay the original result
|
||||
for a duplicate request id, and return `409` with the current projection for a
|
||||
stale revision or an unconfirmed unfinished-goal replacement. These controls do
|
||||
not create issue comments.
|
||||
|
||||
### 10.4.1 Atomic Checkout Contract
|
||||
|
||||
|
|
@ -1163,6 +1188,17 @@ interface AgentAdapter {
|
|||
}
|
||||
```
|
||||
|
||||
### Local adapter engine availability
|
||||
|
||||
For the legacy Codex, Claude, Gemini, and Kimi local adapters, an omitted engine
|
||||
or legacy `auto` value selects ACP deterministically. Missing prerequisites or
|
||||
ACP execution failures fail the run; they must not launch a different engine
|
||||
with different session, permission, or sandbox semantics. CLI execution requires
|
||||
explicit selection. Environment tests report the same engine availability error
|
||||
as execution. Codex CLI defaults permit workspace writes and network access for
|
||||
Paperclip coordination without disabling its sandbox; explicit operator
|
||||
restrictions and execution-target network denials remain effective.
|
||||
|
||||
## 11.2 Process Adapter
|
||||
|
||||
Config shape:
|
||||
|
|
@ -1231,6 +1267,35 @@ Scheduler must skip invocation when:
|
|||
- an existing run is active
|
||||
- hard budget limit has been hit
|
||||
|
||||
## 11.7 Durable agent session goals
|
||||
|
||||
Runner Protocol v2 negotiates a required `sessionGoals` capability and typed
|
||||
`session.goal.*` commands and events. PRP v1 sessions remain supported and are
|
||||
goal unsupported. The Codex app-server driver maps controls to
|
||||
`thread/goal/get`, `thread/goal/set`, and `thread/goal/clear`; it observes
|
||||
provider-created goal notifications and reconciles with an authoritative get
|
||||
after each turn. An active goal suppresses premature run terminalization while
|
||||
autonomous turns continue. The Paperclip runner's persistent ACP backend opts
|
||||
in through the `_session/goal` extension and advertises its exact action subset.
|
||||
Its pinned Codex/Claude executables retain the runner's Linux x64 qualification
|
||||
requirement. Direct `codex_local` and `claude_local` adapters currently have no
|
||||
live goal controller and remain unsupported, even when their underlying ACP
|
||||
package exposes goals. Goal actions never change an agent's adapter, model,
|
||||
permission policy, or rollout settings to manufacture support. CLI, one-shot
|
||||
ACP, and providers without the structured extension remain unsupported.
|
||||
|
||||
When a goal heartbeat settles, the runner suspends its durable authority even
|
||||
under a warm lifecycle policy. Paused, blocked, completed, and rollover goals
|
||||
must survive controller restart without relying on an in-memory warm owner.
|
||||
The next run resumes the same provider session through the existing verified
|
||||
checkpoint and authority-rotation path.
|
||||
|
||||
The board composer treats `/goal` as an action command rather than Markdown or
|
||||
comment text. It is capability-aware, and the issue thread renders durable goal
|
||||
status and controls immediately above the composer. Goal completion enters the
|
||||
normal run-result/completion arbitration path and does not directly close the
|
||||
issue.
|
||||
|
||||
## 12. Governance and Approval Flows
|
||||
|
||||
## 12.1 Hiring
|
||||
|
|
@ -1259,6 +1324,22 @@ Board can at any time:
|
|||
- edit budgets and limits
|
||||
- approve/reject/cancel pending approvals
|
||||
|
||||
## 12.4 Connection Tool Reviews
|
||||
|
||||
Ask-first connection calls use a server-owned tool-action confirmation linked to
|
||||
the authoritative action request. The task feed retains a stable record; dismissal
|
||||
only hides the composer takeover. Task and Connections decisions share one
|
||||
transaction. Approval runs stored, signed arguments once; decline runs nothing.
|
||||
The human decision remains distinct from provider execution success or failure.
|
||||
|
||||
Always allow remembers the same agent, connection, and action, restricted to the
|
||||
current project when present, with future argument values permitted. Explicit
|
||||
denials, revoked access, catalog-definition changes, and formal approval gates
|
||||
remain effective. A durable continuation receipt resumes eligible task context
|
||||
with the recorded outcome after the agent yields. Uncertain interrupted execution
|
||||
is surfaced without automatic replay. See [Task reviews](connections/TASK-REVIEWS.md)
|
||||
for contracts, recovery behavior, Storybook, and acceptance workflows.
|
||||
|
||||
## 13. Cost and Budget System
|
||||
|
||||
## 13.1 Budget Layers
|
||||
|
|
@ -1492,3 +1573,12 @@ Export/import behavior in V1:
|
|||
- import supports preview (dry-run) before apply
|
||||
- import preview reports skill-policy and legacy-grant mappings before apply and rejects unknown policy schema versions
|
||||
- GitHub imports warn on unpinned refs instead of blocking
|
||||
|
||||
### User messages after native execution recovery stops
|
||||
|
||||
An authenticated user message can start a fresh native conversation turn once
|
||||
the prior execution is confirmed stopped. Retain the source history and uncertain
|
||||
action outcomes; do not replay tool calls or reset the failed incident's automatic
|
||||
retry budget. Existing pause, approval, budget, ownership, and dependency gates
|
||||
remain in effect. See `doc/execution-semantics.md` for admission and stop-proof
|
||||
requirements.
|
||||
|
|
|
|||
23
doc/SPEC.md
23
doc/SPEC.md
|
|
@ -29,6 +29,11 @@ Every Company has a **Board** that governs high-impact decisions. The Board is t
|
|||
- CEO's initial strategic breakdown (CEO proposes, Board approves before execution begins)
|
||||
- [TBD: other governance-gated actions — goal changes, firing Agents?]
|
||||
|
||||
Connection tool reviews also appear in task history, with a composer takeover for
|
||||
human approval, decline, or scoped remembered permission. Connections and task
|
||||
views resolve the same review, and the agent continues with the server-recorded
|
||||
outcome. See [the implementation contract](SPEC-implementation.md#124-connection-tool-reviews).
|
||||
|
||||
#### Board Powers (Always Available)
|
||||
|
||||
The Board has **unrestricted access** to the entire system at all times:
|
||||
|
|
@ -171,6 +176,11 @@ When a task originates from a cross-team request, track the **depth** as an inte
|
|||
|
||||
#### Billing Codes
|
||||
|
||||
Task detail keeps hierarchy separate from creation provenance: the Tasks tab shows
|
||||
all subtasks and, independently, work created from the current task grouped by
|
||||
project or No project. A created subtask may appear in both sections. Creation
|
||||
provenance follows the originating run equally for legacy and native runners.
|
||||
|
||||
Tasks carry a **billing code** so that token spend during execution can be attributed upstream to the requesting task/agent. When Agent A asks Agent B to do work, the cost of B's work is tracked against A's request. This enables cost attribution across the org.
|
||||
|
||||
### Open Questions
|
||||
|
|
@ -204,6 +214,12 @@ Agent configuration includes an **adapter** that defines how Paperclip invokes t
|
|||
|
||||
The `process` and `http` adapters ship as generic defaults. Additional built-in adapters cover common local coding runtimes (see list above), and new adapter types can be registered via the plugin system (see Plugin / Extension Architecture).
|
||||
|
||||
An adapter's selected execution engine is part of its permission and session
|
||||
contract. Missing prerequisites or engine failures must be surfaced without
|
||||
silently launching a different engine. A default local engine must support
|
||||
normal task work and control-plane coordination; explicit operator restrictions
|
||||
remain authoritative.
|
||||
|
||||
### Adapter Interface
|
||||
|
||||
Every adapter implements three methods:
|
||||
|
|
@ -532,3 +548,10 @@ Things Paperclip explicitly does **not** do:
|
|||
7. **Atomic ownership.** Single assignee per task. Atomic checkout prevents conflicts.
|
||||
8. **Progressive deployment.** Trivial to start local, straightforward to scale to hosted.
|
||||
9. **Extensible core.** Clean boundaries so plugins can add capabilities (Adapters, knowledge base, revenue tracking) without modifying core.
|
||||
|
||||
### Paused task messages
|
||||
|
||||
A paused task takes over the composer with an amber notice and a Resume action.
|
||||
Operators must release the effective task or ancestor pause before sending a new
|
||||
message. The draft stays intact. This applies to both task interfaces and to
|
||||
board comment API requests; an agent may still report interrupted work.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
# Storybook branch hosting
|
||||
|
||||
The `Storybook Deploy` workflow publishes public static Storybook builds to the
|
||||
existing private S3 bucket behind CloudFront. It does not deploy to GitHub Pages.
|
||||
|
||||
## Current destination
|
||||
|
||||
- AWS account: `078455283791`, region `us-east-1`
|
||||
- Bucket: `paperclipai-runner-e2e-history-078455283791-us-east-1`
|
||||
- Allowed upload prefix: `storybook/branches/`
|
||||
- Distribution: `E3GTU28BBO2SFR`
|
||||
- Public origin: `https://d1p6rlowie26tp.cloudfront.net`
|
||||
- Role: `arn:aws:iam::078455283791:role/paperclip-storybook-github`
|
||||
|
||||
The distribution's default behavior disables edge caching and rewrites directory
|
||||
URLs to `index.html`. Stable branch indexes send `no-cache`; unique build objects
|
||||
send `immutable`. No invalidations or CloudFront write permissions are needed.
|
||||
|
||||
Each publication updates a readable bookmark, such as
|
||||
`https://d1p6rlowie26tp.cloudfront.net/storybook/branches/master/`, after the
|
||||
immutable build upload completes. It also updates the previous hashed branch
|
||||
entry for compatibility. The run summary and Markdown artifact link the bookmark.
|
||||
Branch names use one escaped path segment, preserving case and separating slashes
|
||||
from hyphens; see [the branch publishing guide](DEVELOPING.md#publish-a-branch-storybook)
|
||||
for the encoding. No additional AWS permissions or distribution changes are needed.
|
||||
|
||||
## GitHub configuration
|
||||
|
||||
Create environment `storybook-deploy` with required reviewers set to the
|
||||
individual CODEOWNERS accounts. Disable administrator bypass, allow self-review,
|
||||
and allow repository branches. Keep these reviewers synchronized with CODEOWNERS.
|
||||
The workflow rejects environments with no required reviewers, non-owner reviewers
|
||||
or administrator bypass enabled. The AWS role trusts only this repository and
|
||||
this environment, so a branch cannot obtain upload access through an unprotected
|
||||
environment.
|
||||
|
||||
Set repository variables:
|
||||
|
||||
| Variable | Value |
|
||||
| --- | --- |
|
||||
| `STORYBOOK_AWS_ROLE_ARN` | `arn:aws:iam::078455283791:role/paperclip-storybook-github` |
|
||||
| `STORYBOOK_AWS_REGION` | `us-east-1` |
|
||||
| `STORYBOOK_S3_BUCKET` | `paperclipai-runner-e2e-history-078455283791-us-east-1` |
|
||||
| `STORYBOOK_PUBLIC_BASE_URL` | `https://d1p6rlowie26tp.cloudfront.net` |
|
||||
|
||||
No stored AWS access keys are needed. Leave the runner dashboard variables and
|
||||
GitHub Pages configuration unchanged.
|
||||
|
||||
## Operator setup
|
||||
|
||||
Use the `paperclip-dev` operator AWS profile. Review the checked-in policies in
|
||||
`.github/storybook-deploy/` before applying them. The existing GitHub OIDC provider
|
||||
must be present in this account.
|
||||
|
||||
```sh
|
||||
aws sts get-caller-identity --profile paperclip-dev
|
||||
aws iam create-role --profile paperclip-dev \
|
||||
--role-name paperclip-storybook-github \
|
||||
--assume-role-policy-document file://.github/storybook-deploy/trust-policy.json
|
||||
aws iam put-role-policy --profile paperclip-dev \
|
||||
--role-name paperclip-storybook-github --policy-name StorybookBranchUpload \
|
||||
--policy-document file://.github/storybook-deploy/upload-policy.json
|
||||
```
|
||||
|
||||
For an existing role, use `update-assume-role-policy` instead of `create-role`.
|
||||
Add the statement from `cloudfront-read-statement.json` to the existing bucket
|
||||
policy's `Statement` array. Preserve every other statement, including the HTTPS
|
||||
requirement and runner report access. Keep all S3 public-access blocks enabled;
|
||||
only CloudFront receives read access to this public-content prefix.
|
||||
|
||||
The role has no delete, bucket policy, IAM, CloudFront, or root-object permissions.
|
||||
The workflow never runs `sync --delete`. Builds accumulate; any retention cleanup
|
||||
must preserve the build referenced by each branch entry.
|
||||
|
||||
## Verification
|
||||
|
||||
```sh
|
||||
node --test scripts/__tests__/storybook-deploy.test.mjs
|
||||
actionlint .github/workflows/storybook-deploy.yml .github/workflows/storybook-visual.yml
|
||||
```
|
||||
|
||||
Dispatch two source branches, approve each deployment, and check their distinct
|
||||
branch URLs and each build's `deployment.json`. Redeploy one branch and confirm
|
||||
its stable URL now points to the new build while the other branch is unchanged.
|
||||
The publisher checks the public build metadata against the selected source SHA
|
||||
and verifies that the public branch entry points to this exact build. It retries
|
||||
brief propagation delays and fails if the branch URL remains stale.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 169 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
|
|
@ -0,0 +1,147 @@
|
|||
# Cloud build readiness
|
||||
|
||||
The `Cloud readiness` workflow starts for every master push. Its versioned
|
||||
`Cloud deployable v1` job succeeds only after all three prerequisites succeed:
|
||||
|
||||
- The existing `Release Verify` workflow checks that exact commit, including
|
||||
typecheck, builds, general and serialized tests, and Runner verification.
|
||||
- The reusable `Docker cloud` workflow builds and verifies its Linux AMD64
|
||||
image, including Sentry resolution and orphan reaping, then publishes the
|
||||
full-SHA cloud tag. Cloud readiness owns the master trigger so there is one
|
||||
cloud build per push. Release tags and manual Docker runs retain their callers.
|
||||
- The full-SHA image and both exact-source npm packages are visible. The
|
||||
packages are `@paperclipai/shared` and `@paperclipai/db` at
|
||||
`0.0.0-preview.g<FULL_SHA>`, published through the migrator-only release lane.
|
||||
Registry metadata must match the full commit, and the database package must
|
||||
pin the matching shared package.
|
||||
|
||||
The Cloud workflow builds the image with `USER_UID=1001` and `USER_GID=1001`,
|
||||
matching the managed runtime. This avoids a startup user remap, which can walk
|
||||
the mounted home and delay health checks. Before publishing the full-SHA tag,
|
||||
the workflow checks the baked identity without running the entrypoint, then
|
||||
checks the normal entrypoint's effective user and writable home. Volume ownership
|
||||
repair still runs when needed. The Dockerfile defaults remain `1000:1000` for
|
||||
self-hosted builds, and runtime identity overrides remain supported. The first
|
||||
build with the new identity must rebuild layers that depend on the base image;
|
||||
later builds can reuse those layers.
|
||||
|
||||
Verification and image building run concurrently, outside the full npm release's
|
||||
concurrency group. Different commits have independent groups. The npm canary
|
||||
release reuses `Cloud source verified v1` for the exact master push instead of
|
||||
starting a second copy of `Release Verify`. This source-only job depends on every
|
||||
source check but does not wait for Docker or migrator publication. npm canary
|
||||
publication remains possible when source verification passes and an image build
|
||||
fails. Stable releases and candidate-branch betas still run full verification.
|
||||
|
||||
The canary consumer requires the expected workflow ID and path, upstream source
|
||||
repository, master push event, full SHA, and a successful job in the latest run
|
||||
attempt. It checks the run again after reading the jobs to reject a concurrent
|
||||
rerun. Missing proof waits for up to 45 minutes; failed, skipped, cancelled,
|
||||
ambiguous, or mismatched proof cannot authorize publication. API failures fail
|
||||
closed. If a source check fails, fix it and rerun Cloud readiness before retrying
|
||||
the release. Use **Re-run all jobs** when a later attempt did not rerun the source
|
||||
proof; an earlier attempt's successful job is not accepted. This avoids duplicate
|
||||
test jobs on standard runners. Measure queue time to assess the timing gain.
|
||||
|
||||
Release verification spreads the general server suites across ten standard hosted
|
||||
runners, with the long chat suite split separately across three jobs. Each server
|
||||
job still runs one test worker. The partition covers every suite exactly once;
|
||||
normal PR and local test groups keep their existing shape. More jobs increase
|
||||
concurrent runner demand, so compare queue time as well as test duration.
|
||||
|
||||
The artifact wait runs for up to 30 minutes and reports what is missing. Only
|
||||
an HTTP 404 means publication is pending; authorization errors, upstream outages,
|
||||
and identity mismatches fail the job. A failed, cancelled, or skipped prerequisite
|
||||
cannot produce a successful readiness job. Retry the failed publication or build,
|
||||
then rerun the failed readiness workflow jobs to check the same commit again.
|
||||
|
||||
## Consumer contract
|
||||
|
||||
`Cloud deployable v1` is a source-and-artifact readiness signal. A deployment
|
||||
consumer must still resolve and pin the image digest and npm integrity/lockfile,
|
||||
validate migration contents and compatibility, and apply its target health gates.
|
||||
The check creates no release record and deploys no instance. A full-SHA tag by
|
||||
itself, or a successful migrator dispatch, is not this readiness signal.
|
||||
|
||||
For automatic selection, accept only a successful job named exactly
|
||||
`Cloud deployable v1` in the latest attempt of a successful
|
||||
`.github/workflows/cloud-readiness.yml` run in `paperclipai/paperclip`, with
|
||||
event `push`, head branch `master`, and the expected full head SHA and repository.
|
||||
Do not trust a similarly named check from another workflow or a manual branch run.
|
||||
Order candidates by master ancestry, not job completion time: an older commit
|
||||
finishing late must not roll a fleet backward. Fail closed on API errors.
|
||||
|
||||
Existing npm canary discovery is unchanged by this producer workflow. Consumers
|
||||
can adopt the versioned signal separately after the workflow has landed and
|
||||
successfully verified a real master commit.
|
||||
|
||||
## Timing and rollout
|
||||
|
||||
The reusable Runner chaos workflow scopes concurrency to the caller workflow
|
||||
and source ref. Cloud readiness, stable verification, and standalone evals can
|
||||
verify the same commit at the same time. They must not cancel each other's
|
||||
required test job.
|
||||
|
||||
Measure the complete path from a master merge to a healthy target running that
|
||||
exact commit. Keep readiness and deployment as separate milestones:
|
||||
|
||||
| Milestone | Evidence | Elapsed time starts at |
|
||||
| --- | --- | --- |
|
||||
| Merge | Merged PR timestamp and full merge commit SHA | Merge |
|
||||
| Image available | Successful full-SHA image publication and verification | Merge |
|
||||
| Cloud deployable | Successful `Cloud deployable v1` job in the accepted push run and attempt | Merge |
|
||||
| Canary healthy | Deployment consumer's canary health gate confirms the target commit | Merge |
|
||||
| Fleet complete | Campaign succeeds for all eligible targets at that commit | Merge |
|
||||
|
||||
Record the source SHA, workflow run ID and attempt, readiness job completion
|
||||
time, and deployment campaign identity together. Verify the run against the
|
||||
consumer contract above. A manual dispatch can test wiring, but its timestamp
|
||||
does not measure automatic merge-to-deploy latency. A preparation-only run
|
||||
resolves artifacts without deploying a target and must not be counted as a
|
||||
successful deployment.
|
||||
|
||||
Record queue time and the image, source-verification, and artifact-wait durations
|
||||
separately. The slowest prerequisite determines readiness; shortening an already
|
||||
faster prerequisite may have no effect on the total. After readiness, measure
|
||||
consumer discovery delay, artifact resolution, canary health, and fleet rollout.
|
||||
An automatic consumer that still waits for the full npm canary publication has
|
||||
that queue on its critical path even if cloud artifacts are ready earlier.
|
||||
|
||||
For a target health measurement, confirm the deployed source SHA as well as
|
||||
service health. A proxy health response alone may describe the control plane
|
||||
while the tenant still runs the previous image. Report the eligible target count,
|
||||
excluded or sleeping targets, retries, and failures with the fleet result. Record
|
||||
runner queue conditions and cache state; one warm or cold run is a sample, not a
|
||||
latency guarantee.
|
||||
|
||||
Land full-SHA image publication, independent cloud builds, and migrator-only
|
||||
publication before enabling this workflow. Until those producers are present,
|
||||
the artifact wait cannot succeed. A manual dispatch on master can verify the
|
||||
wiring, but automatic consumers should use push runs. Source verification and
|
||||
registry checks can be rerun without deploying or changing mutable npm channels.
|
||||
|
||||
When reverting this workflow, restore the master push trigger in
|
||||
`docker-cloud.yml` in the same change so master images continue to build.
|
||||
|
||||
## AWS cloud build routing
|
||||
|
||||
`AWS_CLOUD_BUILDS_ENABLED=true` routes the Docker cloud job to the
|
||||
`paperclip-cloud-build-x64` RunsOn Fleet for canonical `paperclipai/paperclip`
|
||||
master pushes and manual master runs. Forks, pull requests, and release tags
|
||||
retain GitHub-hosted runners. The separate `AWS_CI_ENABLED` and
|
||||
`AWS_CI_TRUSTED_USER_IDS` variables control PR routing.
|
||||
|
||||
The cloud Fleet uses a separate runner group, `paperclip-cloud-build`, restricted
|
||||
to this repository and `.github/workflows/docker-cloud.yml@refs/heads/master`.
|
||||
Provision that group and Fleet before enabling the variable. The cloud runners
|
||||
need at least 64 GiB free for Docker and the workspace; the initial configuration
|
||||
uses 120 GiB disks with the existing 4-vCPU, 16-GiB machine size. AWS jobs have
|
||||
a 40-minute workflow timeout so they finish before the 45-minute instance
|
||||
lifetime; GitHub-hosted jobs retain their 60-minute timeout. Keep the registry
|
||||
cache and all pushed-image verification steps enabled.
|
||||
|
||||
To roll back routing, set `AWS_CLOUD_BUILDS_ENABLED=false`, then rerun the cloud
|
||||
workflow. Changing the variable does not migrate an already assigned job.
|
||||
Check the Actions job's runner name and runner group to verify placement. Record
|
||||
queue time, image verification completion, and `Cloud deployable v1` separately;
|
||||
source verification and the migrator still run on GitHub-hosted runners.
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Cloud UI snippet
|
||||
|
||||
Cloud operators can set `PAPERCLIP_CLOUD_UI_SNIPPET` to an HTML snippet.
|
||||
The server inserts it before `</body>` in static and Vite-served UI pages.
|
||||
It requires the existing Cloud-managed instance signal. Self-hosted instances
|
||||
ignore this setting. No snippet is enabled by default.
|
||||
|
||||
This is trusted deployment configuration, not user input. It executes in the
|
||||
application origin and is visible to every browser that receives the UI shell.
|
||||
Do not include secrets or customer data. Restart the app after changing it.
|
||||
Operators must review scripts and any required CSP changes before deployment.
|
||||
|
||||
## Plain closed beta
|
||||
|
||||
Set the value to this standard embed, replacing `YOUR_CHAT_APP_ID` with the
|
||||
public chat app ID for the target environment:
|
||||
|
||||
```html
|
||||
<script>
|
||||
(function(d) {
|
||||
var script = d.createElement('script');
|
||||
script.src = 'https://chat.cdn-plain.com/index.js';
|
||||
script.onload = function() { Plain.init({ appId: 'YOUR_CHAT_APP_ID' }); };
|
||||
d.head.appendChild(script);
|
||||
})(document);
|
||||
</script>
|
||||
```
|
||||
|
||||
No signing secret or Plain API key is required. No Paperclip customer identity
|
||||
or organization data is passed. Plain manages the anonymous browser session;
|
||||
there is no Paperclip account-switch integration. Ask users for identifying
|
||||
information when needed. The existing feedback flag remains unchanged.
|
||||
|
||||
Docs: [Plain chat](https://www.plain.com/docs/product/channels/chat).
|
||||
|
||||
## Verification and rollback
|
||||
|
||||
On staging, open `/`, `/index.html`, and an organization dashboard directly.
|
||||
Confirm the bubble appears and a test message reaches Plain. Verify the support
|
||||
reply returns. On a self-hosted instance, confirm no snippet or widget is loaded.
|
||||
Unset the snippet and restart to remove it on the next page load. Existing open
|
||||
tabs retain the widget until refreshed. No production deployment is implied.
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
# Composer Stop and task controls
|
||||
|
||||
The empty composer shows **Stop** while this task has a live execution and the
|
||||
viewer can manage task controls. Stop creates the same manual pause hold as
|
||||
**Pause work** / **Pause subtree** in the task menu. A parent pause includes its
|
||||
descendants. Cancellation remains a separate menu action.
|
||||
|
||||
Text (after trimming) or attachments switch the button back to Send. Uploading
|
||||
and failed attachments retain their existing restrictions. Keyboard submission
|
||||
never invokes Stop. Queued-message editing and structured interactions keep
|
||||
their existing actions, and text entered while stopping remains in the draft.
|
||||
|
||||
Pause dispatches without a preview dialog or reason. The button stays pending
|
||||
while affected run state is checked; native cancellation must be acknowledged.
|
||||
A saved hold with unconfirmed termination produces an error rather than a
|
||||
success claim. The cancellation dialog requires a valid preview and excludes
|
||||
terminal tasks. Resume/restore retain the optional wake-agents checkbox.
|
||||
|
||||
## Resume and execution recovery
|
||||
|
||||
Resume releases a hold. Waking agents is optional and only applies to tasks in
|
||||
`todo`, `in_progress`, or `in_review`; parked and terminal tasks stay untouched.
|
||||
The current execution-recovery policy requires verified outcomes before a stopped
|
||||
provider can restart. If any affected task still needs that reconciliation,
|
||||
Resume with wake enabled returns an inline error and preserves the pause. The
|
||||
operator can release the pause without waking agents, then use the existing
|
||||
execution-reconciliation flow after reviewing the stopped run. Resume does not
|
||||
claim that unknown provider actions completed or were never performed.
|
||||
|
||||
A completed release remains successful if a best-effort wake fails. Its response
|
||||
includes optional `wakeFailures`, the page reports them inline, and remaining
|
||||
eligible tasks still receive their wake requests. No new endpoint is introduced.
|
||||
The deterministic E2E fixtures prove interruption, then record their known lack
|
||||
of external effects through the existing reconciliation API before continuing.
|
||||
|
||||
Embedded ACP also supports verified continuation of an interrupted local session.
|
||||
The adapter must acknowledge cancellation and prove a preserved session with
|
||||
settled read-only work. Stop waits for provider cleanup. Forced local termination uses the actual
|
||||
child-process handle captured at spawn, including on Windows. An unavailable
|
||||
handle does not authorize a signal or replay. Unknown actions remain
|
||||
blocked, and task detail shows the reason even after recovery bookkeeping resolves.
|
||||
A run-level Stop leaves the task unpaused; a subsequent comment can continue the
|
||||
same session with the earlier queued messages. Composer Stop still creates a
|
||||
pause hold. New board messages require Resume first. Both comment creation and
|
||||
updates that include a comment return `409` while an effective task or ancestor
|
||||
pause hold is active. Interrupted agents may still report their results. Neither path permits a fresh-session fallback
|
||||
when the interrupted checkpoint cannot be restored.
|
||||
|
||||
The credential-free ACP regression journey uses an actual ACP child process:
|
||||
|
||||
```sh
|
||||
pnpm exec playwright test --config tests/e2e/playwright.config.ts tests/e2e/acp-stop-continuation.spec.ts
|
||||
```
|
||||
|
||||
It covers the queued-follow-up sequence (queue a second request, stop, then send “go”),
|
||||
same-session delivery of both messages, and an unfinished write that stops
|
||||
mutating its file but retains a visible execution blocker. Unit and integration
|
||||
tests additionally cover pre-start Stop, unavailable/changed sessions, rotating
|
||||
scratch directories, cancellation acknowledgment, a provider that hangs during
|
||||
cleanup after returning cancellation, deferred-wake adoption, and
|
||||
company-scoped blocker lookup. Hosted-provider behavior is a separate smoke test.
|
||||
|
||||
The browser tests also require the continued provider to complete the task through
|
||||
the agent API. Restoring a session must refresh its run identity, API credential,
|
||||
and scratch environment. The same conversation must not reuse the stopped run's
|
||||
credential. A regression test checks distinct run IDs and token hashes across the
|
||||
restart without logging the credentials themselves.
|
||||
|
||||
Historical behavior, superseded by the composer takeover: on 2026-09-09, all three ACP browser journeys passed. A manual browser walk-through
|
||||
also queued a request, used composer Stop, sent “go” while paused, and selected
|
||||
Resume work. The pause stayed in place during the conversation reply. Resume
|
||||
restored the same provider session, answered the pending request once, and moved
|
||||
the task to Done through the current run's authenticated API call. These tests use
|
||||
a deterministic ACP child process; they do not call Drive or another external app.
|
||||
The manual unfinished-write check also confirmed that file size stayed unchanged
|
||||
for five seconds after Interrupt. Sending “go” displayed the reconciliation reason
|
||||
and did not start another provider prompt.
|
||||
|
||||
A separate live Claude ACP smoke test interrupted a Bash tool writing only to a
|
||||
disposable local file: cancellation settled in 1,167 ms, output remained unchanged
|
||||
for five seconds, and no matching tool process remained. The shell action correctly
|
||||
did not receive automatic replay permission. A second live Claude check interrupted
|
||||
a response with no tools, restored the exact same provider session, and received
|
||||
the requested follow-up answer. These are local-provider observations, not a
|
||||
latency guarantee or proof for every provider and remote sandbox.
|
||||
|
||||
## Quiet task feedback
|
||||
|
||||
The visible task/subtree does not produce duplicate state toasts. Its live
|
||||
notifications are suppressed while foregrounded, including descendant runs;
|
||||
unrelated and background work retains notifications. Tree-control results use
|
||||
inline state, and failures stay in the composer, page, or confirmation dialog.
|
||||
The amber composer takeover contains “Subtree is paused.” (or “Task is paused.”),
|
||||
a short instruction to resume before sending, and Resume. It replaces input
|
||||
controls in both task interfaces, cannot be dismissed, and preserves text and
|
||||
attachment drafts. An inherited hold links to the ancestor task. Pending resume
|
||||
keeps the takeover visible; failed resume leaves the task paused. Expected cancellation uses a muted gray disclosure with optional details.
|
||||
This is recorded as a product rule in `DESIGN.md`.
|
||||
|
||||
The follow-up passed 213 focused tests, both isolated runner E2E journeys
|
||||
(including no-toast assertions), UI typecheck/build, token gates, and Storybook
|
||||
build. Paused, expanded cancellation, Stop without toasts, mobile, and light
|
||||
stories were inspected in the browser.
|
||||
|
||||
## Storybook
|
||||
|
||||
Run from the worktree:
|
||||
|
||||
```sh
|
||||
pnpm --filter @paperclipai/ui exec storybook dev -p 6016 -c storybook/.storybook --no-open
|
||||
```
|
||||
|
||||
Open `http://localhost:6016/?path=/story/tasks-execution-controls--running-empty`.
|
||||
The `Tasks / Execution Controls` stories compose the production composer and
|
||||
menu/dialog controls together. They cover text switching, attachment-only,
|
||||
idle, stopping, paused, errors, cancellation preview/loading, and mobile/light
|
||||
presentations. The Storybook state transitions simulate requests; runner
|
||||
verification belongs to the isolated browser suite below.
|
||||
|
||||
## Automated verification
|
||||
|
||||
```sh
|
||||
pnpm exec vitest run --project @paperclipai/ui ui/src/components/task-chat/TaskChatComposer.test.tsx ui/src/components/TaskChatThread.test.tsx ui/src/pages/IssueDetail.test.tsx ui/src/lib/wait-for-stopped-runs.test.ts
|
||||
pnpm exec vitest run --project @paperclipai/server server/src/__tests__/issue-tree-control-routes.test.ts
|
||||
pnpm check:token-gates
|
||||
pnpm build-storybook
|
||||
pnpm -r typecheck
|
||||
pnpm test:run
|
||||
pnpm build
|
||||
```
|
||||
|
||||
The component/page tests cover whitespace, attachments and upload restrictions,
|
||||
permissions, keyboard submission, queue edits, duplicate clicks, draft
|
||||
preservation, shared pause requests, compact cancellation, and visible errors.
|
||||
Stop verification tests include continued execution, native acknowledgment,
|
||||
network failures, and hung status requests. Route tests cover pause dispatch,
|
||||
authorization, cancellation, and opt-in resume wakeups with terminal/company
|
||||
exclusions.
|
||||
|
||||
## Isolated browser acceptance
|
||||
|
||||
Legacy process coverage needs no provider credentials:
|
||||
|
||||
```sh
|
||||
pnpm exec playwright test --config tests/e2e/playwright-composer-stop.config.ts
|
||||
```
|
||||
|
||||
To include native execution, build the real runner and deterministic Codex
|
||||
protocol fixture from this checkout, then point the suite at their absolute
|
||||
paths:
|
||||
|
||||
```sh
|
||||
cargo build --manifest-path packages/paperclip-runner/runner/Cargo.toml --bin paperclip-runnerd --bin fake-codex-app-server
|
||||
PAPERCLIP_STOP_FAKE_CODEX="$PWD/packages/paperclip-runner/runner/target/debug/fake-codex-app-server" \
|
||||
PAPERCLIP_RUNNER_BINARY="$PWD/packages/paperclip-runner/runner/target/debug/paperclip-runnerd" \
|
||||
pnpm exec playwright test --config tests/e2e/playwright-composer-stop.config.ts
|
||||
```
|
||||
|
||||
The suite boots a disposable local-trusted instance on port 3199 (override with
|
||||
`PAPERCLIP_E2E_PORT`). It never attaches to an existing server. Native coverage
|
||||
is explicitly skipped without the fixture; it must not use a logged-in provider
|
||||
as a fallback. Test companies are archived during cleanup.
|
||||
|
||||
For each runner, the journey starts a parent, child, and unrelated task, plus a
|
||||
terminal child. It sends while running and verifies the durable queue, clicks
|
||||
Stop, verifies interruption and the persisted hold, and observes three
|
||||
ten-second scheduler intervals without continuation. It resumes with wakeups,
|
||||
pauses from the menu, dismisses and then confirms cancellation, and verifies
|
||||
terminal-task exclusions and unrelated execution.
|
||||
|
||||
Timing attachments distinguish click-to-request from request-to-observed-stop.
|
||||
Native proof requires provider `turn/start` before the click, `turn/interrupt`
|
||||
after it, and durable cancellation acknowledgment. Legacy proof checks the
|
||||
actual parent and child PIDs have exited with a one-second configured grace
|
||||
period. HTTP success alone is insufficient.
|
||||
|
||||
For a release with real providers, repeat while an actual long-running tool is
|
||||
active, confirm the tool's own process/output stops, and verify a queued message
|
||||
is consumed on continuation. The deterministic provider exercises the native
|
||||
transport and cancellation protocol, not external provider behavior or every
|
||||
tool's process cleanup. Preserve these limits in any test report.
|
||||
|
||||
## Recorded acceptance run (2026-09-09)
|
||||
|
||||
Both isolated browser journeys passed. Legacy Stop dispatched after 214 ms and
|
||||
observed both runs stopped 155 ms after dispatch; native dispatched after 212 ms
|
||||
and observed stop 320 ms later. These are single local observations including
|
||||
browser automation overhead, not latency guarantees. Native used the real
|
||||
runnerd and the repository's deterministic Codex protocol fixture. Real hosted
|
||||
provider/tool cleanup remains a release acceptance check.
|
||||
|
||||
Repository typecheck, production build, Storybook build, token gates, targeted
|
||||
UI tests, and tree-control route tests passed. The broad UI run passed 5,491
|
||||
tests with one five-second timeout in an unchanged `IssuesList` test; rerunning
|
||||
that file passed all 46 tests. The repository test run encountered 17 failures
|
||||
in unchanged suites, all reproduced in the original checkout:
|
||||
|
||||
- `server/src/__tests__/workspace-runtime.test.ts`: 2 failures.
|
||||
- `server/src/services/workspace-runtime-exposure.test.ts`: 7 failures.
|
||||
- `server/src/__tests__/execution-workspace-runtime-control-conflict.test.ts`: 4 failures.
|
||||
- `server/src/__tests__/workspace-instance-cleanup.test.ts`: 1 failure.
|
||||
- `server/src/__tests__/company-skills.test.ts`: 2 failures.
|
||||
- `server/src/__tests__/worktree-seed-server-spawn.test.ts`: 1 failure.
|
||||
|
||||
These baseline failures prevent a green repository-wide test result.
|
||||
The broad run was stopped after more than 30 minutes in its serial server lane
|
||||
once these failures were independently reproduced. Later full-suite groups
|
||||
did not run. The full UI suite and the feature's server route suite were run
|
||||
separately as described above.
|
||||
|
||||
The `Tasks / Composer / Paused task takeover` stories use the production composer
|
||||
and cover task/subtree holds, saved drafts, resume progress/failure, and light/mobile layouts.
|
||||
|
|
@ -11,14 +11,18 @@ Connection intents let an agent ask the responsible user for a known service con
|
|||
|
||||
Provider-specific setup must stay in the shared feature and `AppDefinition` metadata. Do not add provider forms or connection mutations to either host.
|
||||
|
||||
When a task connection needs Paperclip Cloud enrollment, the shared dialog opens enrollment in a separate window. The task keeps its access selection and interaction ID. A new-tab link is available if the window does not open. The dialog reads server enrollment status and refreshes the provider catalog after approval; enrollment alone does not mark the app connected. OAuth retains the interaction ID even if setup resumes in the page host, so the verified callback can resolve the task card and queue its continuation.
|
||||
|
||||
## Agent tools
|
||||
|
||||
Every active heartbeat with a responsible user receives two run-bound tools:
|
||||
|
||||
- `connections_search({ query })` searches first-party connectable definitions and returns `ready`, `needs_user_action`, `available`, or `unavailable` from the requesting agent's perspective.
|
||||
- `connection_request({ service })` returns immediately when the service is already usable. Otherwise it creates or reuses a `connection_intent` and instructs the agent to end the run pending continuation.
|
||||
- `connections_search({ query })` searches catalog names and descriptions, plus authorized configured MCP connections and indexed tool descriptions and returns `ready`, `needs_user_action`, `available`, or `unavailable` from the requesting agent's perspective.
|
||||
- `connection_request({ service })` returns immediately when the service is already usable. Otherwise it creates or reuses a `connection_intent` and instructs the agent to finish independent work, then yield pending continuation.
|
||||
|
||||
Claude and Codex receive the tools through a native managed MCP server. Local/process adapters receive `PAPERCLIP_RUNTIME_TOOLS_*` environment variables and CLI guidance. Cloud, HTTP, gateway, and external adapters receive the typed runtime descriptor in their invocation context; compatible adapters may also project it into their remote environment.
|
||||
The native Paperclip Runner advertises both tools through its server-owned tool authority even with an empty MCP assignment. It captures the current responsible identity at each call. Legacy Claude and Codex receive the tools through a managed MCP server. Local/process adapters receive `PAPERCLIP_RUNTIME_TOOLS_*` environment variables and CLI guidance. Cloud, HTTP, gateway, and external adapters receive the typed runtime descriptor in their invocation context; compatible adapters may also project it into their remote environment.
|
||||
|
||||
Legacy delivery uses the same intent service, setup card, and fresh-session resolution wake. Environment and descriptor delivery require the receiving harness to consume them; they do not establish support in every third-party runtime. The default legacy prompt includes the canonical discovery guidance. A custom `promptTemplate` replaces that default and should retain the connection guidance if proactive discovery is desired.
|
||||
|
||||
The equivalent CLI helpers are:
|
||||
|
||||
|
|
@ -31,14 +35,46 @@ The manually configured Paperclip MCP server also advertises `connections_search
|
|||
|
||||
## Security and lifecycle
|
||||
|
||||
- Company, agent, run, task, and responsible user come only from the signed runtime token and stored heartbeat context.
|
||||
- Company, agent, run, task, and responsible user come only from the signed legacy runtime token or the native server-owned binding and stored execution identity.
|
||||
- Tokens are scoped to connection intents, expire after one hour, and are rejected when the heartbeat is no longer running.
|
||||
- The thread payload contains only service identity, requesting-agent identity, and a safe phase. It never contains credentials or authorization URLs.
|
||||
- OAuth state is linked to the interaction. The same-origin callback finalizes the existing connection pipeline, posts only interaction ID/outcome to its opener, and redirects back to the task if there is no opener.
|
||||
- Personal OAuth defaults to the addressed user and creates an explicit delegation to the requesting agent. Reuse and installs are additive.
|
||||
- Task-hosted setup locks install reach to the requesting agent; the store host retains its normal broader access choices.
|
||||
- The intent resolves only after the connection, grant/delegation, profile access, and install succeed. Failures remain pending with `needs_retry`.
|
||||
- Closing the task, a newer run requesting the same service, or a newer human task comment expires the intent and deletes linked OAuth state.
|
||||
- Success and decline wake the assignee once using an interaction-and-status idempotency key and force a fresh continuation session.
|
||||
- Closing or reassigning the task expires pending intents and deletes linked OAuth state. Ordinary comments and later runs preserve the pending card. Requests reuse the same task, requester, addressed user and service; a different addressed user supersedes an older request for that service.
|
||||
- Success and explicit decline atomically persist a continuation delivery with resolution. A leased startup/periodic worker dispatches through heartbeat with a unique `connection-intent:<interaction>:<outcome>` wake key. It checks assignment, status, membership and current executable access, retries paused/suppressed delivery, and recovers a crash after enqueue without creating a second wake. The continuation forces a fresh provider session; heartbeat queues it behind active execution.
|
||||
|
||||
Legacy `request_confirmation.payload.connectionAuthorization` interactions remain readable and resolvable. New agent requests use `connection_intent` exclusively.
|
||||
|
||||
## Model evaluations
|
||||
|
||||
The companion `paperclip-evals` repository owns the connection cases in
|
||||
`evals/runner-api-tools/connection-cases.json`. They use the existing real-server
|
||||
API-tool eval controller and `scripts/runner-api-eval-worker.ts`, with this
|
||||
checkout's production tool definitions, connection guidance, and authority.
|
||||
Natural prompts measure discovery and request selection; explicit contract probes
|
||||
measure deduplication, readiness, and denied targets. The fixture helper supplies
|
||||
isolated company records and retains initial and final connection interaction
|
||||
state. Scoring requires observed calls/results and persisted state, not an
|
||||
assistant's claim of having connected.
|
||||
|
||||
These tool evals do not perform live provider OAuth or establish browser quality.
|
||||
The native and legacy connection browser suites separately cover setup and
|
||||
continuation, while the live-provider journey report records actual authorization
|
||||
and data-read coverage.
|
||||
|
||||
|
||||
## Custom targets, readiness and recovery
|
||||
|
||||
Catalog slugs remain stable. Search also returns `connection:<uuid>` for configured custom connections whose active identity grants authorize the responsible person, their company, or the requesting agent. Identifiers are never interpreted as URLs. Configured metadata and tool descriptions, including catalog-provider descriptions, are read only after the company and identity audience checks. Setup choices expose only display and selection metadata; they never include connection configuration, transport settings, or credential fields. Discovery reads the stored index without refreshing providers. Search is ranked with exact provider matches first and capped at 20 results.
|
||||
|
||||
`ready` requires an installed, enabled, healthy executable connection, permitted catalog tools, and a usable runtime identity. An installed connection with denied actions is administrative denial rather than a request to reauthenticate. Runtime calls continue enforcing access after a historical card resolves. If access is ready but the native provider's pinned tool snapshot is older, `connection_request` queues a fresh session without another authorization card.
|
||||
|
||||
Task setup defaults to personal identity when supported and the requesting agent's install reach. Existing installs are additive. An OAuth callback from a task prepares the catalog without adding access. Intent completion validates the current task and identity, then adds the requesting agent’s binding and install in the resolution transaction. Callback messages do not establish authorization: the card reloads the durable server result. A blocked popup offers a new-tab fallback; closing or declining provider sign-in keeps the request retryable. Only the card's **Not now** action declines the request. Decline continuations are told to pursue alternatives and cannot immediately request the same service again.
|
||||
|
||||
## Verification
|
||||
|
||||
Service and native-authority tests cover discovery, current identity, company boundaries, cross-run deduplication, additive grants and installs, permission denial, resolution atomicity, restart delivery and stale assignment. `tests/e2e/in-feed-native/playwright.config.ts` starts source `test-drive` instances with fresh data directories and a deterministic fake Codex provider plus MCP server. It exercises the real native runner and gateway; it is fixture proof, not live Notion or GitHub proof. Run with `pnpm exec playwright test -c tests/e2e/in-feed-native/playwright.config.ts`.
|
||||
|
||||
Offline Storybook examples live in `ui/storybook/stories/in-feed-connections.stories.tsx`. Build with `pnpm --filter @paperclipai/ui build-storybook`, then run `pnpm exec playwright test -c tests/storybook-visual/in-feed-connections.config.ts`. The suite checks every independently addressable story in both themes, catches play-function failures, and saves screenshots. Live provider acceptance additionally requires a model credential and a test workspace/account; do not describe fixture results or local-trusted testing as authenticated/cloud acceptance.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
# Connection review verification — 2026-09-08
|
||||
|
||||
Implementation workspace: `/Users/dotta/paperclipai/branches/codex/reviews-in-task`.
|
||||
Branch: `codex/reviews-in-task`, rebased on `master` at `8f099c3f8`.
|
||||
The original verification below predates that rebase; final checks are recorded in the PR.
|
||||
|
||||
## Acceptance status
|
||||
|
||||
The deterministic integration paths demonstrate both synchronization directions
|
||||
and scripted-agent continuation. A real native Codex approval and continuation
|
||||
also passed in the local test drive, as recorded below. Live Notion and the
|
||||
remaining real model-runner journeys are **untested dependencies**. The PR records
|
||||
the final repository and CI check results.
|
||||
|
||||
## Inspect the UI
|
||||
|
||||
Storybook is running from this worktree on port 6018:
|
||||
|
||||
- [Interactive task](http://localhost:6018/?path=/story/chat-comments-connection-reviews--interactive-task)
|
||||
- [All card states](http://localhost:6018/?path=/story/chat-comments-connection-reviews--all-states)
|
||||
- [Connections queue](http://localhost:6018/?path=/story/chat-comments-connection-reviews--connections-queue)
|
||||
- [Narrow layout](http://localhost:6018/?path=/story/chat-comments-connection-reviews--narrow)
|
||||
|
||||
Manual browser inspection covered light/dark presentation, the split approval
|
||||
menu, keyboard approval, and dismissal/reopening. The final card shows only the app
|
||||
icon, request description, and decision controls. Optional labels and details were
|
||||
removed. Narrow controls fit without horizontal clipping.
|
||||
The fixture queue can be resolved interactively to inspect its empty state.
|
||||
|
||||
## Deterministic browser journeys
|
||||
|
||||
Each journey creates a company, agent, and custom MCP connection in an isolated
|
||||
embedded database. Ask first is configured through the permissions UI. The provider
|
||||
returns fixture page names; **these are not real Notion pages**.
|
||||
|
||||
| Journey | Observed provider calls | Verified outcome |
|
||||
| --- | ---: | --- |
|
||||
| Approve in task | 1 | Stored call executes; resumed task posts Roadmap/Meeting notes; Connections pending item clears |
|
||||
| Decline in Connections | 0 | One-click decline; open task updates; resumed agent reports decline |
|
||||
| Always allow | 2 | Initial approved call and a later call with changed arguments; later task has no review |
|
||||
| Provider failure | 1 | Human approval remains recorded; task shows execution failure and resumed agent reports it |
|
||||
| Restart while waiting | 1 | Pending request survives actual server restart; approval executes once and task returns page results |
|
||||
|
||||
All journeys also exercise takeover dismissal/reopening, an ordinary comment while
|
||||
pending, reload, and cross-tab synchronization. Review creation performs zero
|
||||
provider calls. Traces and screenshots accompany each case.
|
||||
|
||||
[Open the evidence gallery](http://127.0.0.1:6020/) or the
|
||||
[Playwright report](http://127.0.0.1:6020/report/). The final run passed all five
|
||||
journeys in 1.4 minutes and released its port after teardown.
|
||||
|
||||
The local evidence directory is `.paperclip-runtime/reviews-evidence/`. It contains
|
||||
the Playwright report, traces/screenshots, focused/full-check logs, baseline logs,
|
||||
and `final-journey-identifiers.json` with request, invocation, interaction, and run IDs
|
||||
from the passing port-3226 run. The report's attachments also contain
|
||||
company/task/agent IDs and provider counts for its own run.
|
||||
|
||||
## Automated checks
|
||||
|
||||
- 384 focused gateway, policy/service, native bridge, and card/queue tests pass.
|
||||
- 177 additional interaction route/service and policy tests pass.
|
||||
- 18 startup tests pass after updating their app mocks with recovery services.
|
||||
- 19 runner-catalog tests pass; the opt-in suite defines 16 local cells.
|
||||
- Added scope/repair regressions pass: another agent/project still asks, explicit
|
||||
denial and changed definitions remain effective, concurrent approval executes
|
||||
once, multiple outcomes share one durable wake, and interrupted execution is
|
||||
never replayed.
|
||||
- Repository `pnpm -r typecheck` and `pnpm build` pass.
|
||||
- Runner harness TypeScript, token gates, migration safety, and Storybook build pass.
|
||||
- `pnpm test:run` was run, but the repository-wide result is not green. Feature
|
||||
failures found in the initial run were corrected and their suites rerun above.
|
||||
Thirteen unrelated failures were reproduced at the same unchanged master commit:
|
||||
two workspace-runtime tests, four workspace-repair/control tests, three runtime
|
||||
exposure tests, two company-skill path tests, one instance-cleanup path test,
|
||||
and one worktree-seed spawn test.
|
||||
The initial general-server lane ended with 5,968 passed, 39 failed, and 31
|
||||
skipped tests across 498 files. Twenty failures were feature changes corrected
|
||||
and verified in focused reruns; six were transient file-resource/runtime-port
|
||||
failures that passed on rerun. The thirteen remaining failures reproduce on
|
||||
master. The fail-fast runner did not reach later workspace/serialized lanes.
|
||||
A complete green repository run is still required before PR-ready handoff.
|
||||
|
||||
## Live provider dependencies
|
||||
|
||||
The normal runner command was attempted with the opt-in flag and stopped with
|
||||
`Missing runner E2E credentials: OPENAI_API_KEY, ANTHROPIC_API_KEY`.
|
||||
|
||||
The normal `test-drive --harness codex` command was also attempted from this
|
||||
worktree. It bootstrapped a fresh isolated instance and reached startup recovery on
|
||||
127.0.0.1:3105, then shut down with `No credential found. Set OPENAI_API_KEY`.
|
||||
No Notion OAuth connection or real Notion page read was performed. The native Codex approval journey subsequently passed using existing local
|
||||
ChatGPT authentication, as recorded below. Native ACPX Claude, legacy Codex CLI,
|
||||
and legacy Claude CLI journeys remain unverified. The scripted process-adapter/browser evidence must not substitute for
|
||||
those 16 acceptance cells or the four-profile real Notion exercise.
|
||||
|
||||
Provide the normal runner/test-drive credential setup and Notion account access
|
||||
to complete those journeys. Secrets should remain in the normal local environment
|
||||
or credential store, not in this report or chat.
|
||||
|
||||
## Final simplified UI verification
|
||||
|
||||
Before the master rebase, 155 component tests and all five browser journeys passed.
|
||||
The final UI checks include the split approval menu, keyboard selection of Always
|
||||
allow, and one-click decline. Evidence is in `.paperclip-runtime/reviews-evidence/minimal/`.
|
||||
The browser run took 2.7 minutes; its restarted server required explicit process
|
||||
cleanup after the tests completed. Live provider and model-runner dependencies
|
||||
remain separate from this deterministic evidence.
|
||||
|
||||
## Native Codex approval and continuation
|
||||
|
||||
A real native Paperclip Runner agent used `gpt-5.6-sol` with existing local
|
||||
ChatGPT authentication. Its initial run discovered the installed MCP fixture
|
||||
action, called it with `query: "10 most recent pages"`, and yielded to a pending
|
||||
server-owned review. The operator approved in the browser. The server executed
|
||||
the stored request and delivered its result to a new native run.
|
||||
|
||||
- Source run: `106278a4-5411-41ba-b2a4-c150cdf7760d`.
|
||||
- Action request: `7da846da-496f-4e6a-a151-0252e10f989c`.
|
||||
- Continuation run: `18e11fe1-bd6a-4034-996e-e635d6cd57a6`.
|
||||
- Final task status: `done`.
|
||||
- Agent response: “The most recent fixture pages are **Roadmap**, **Meeting notes**,
|
||||
and **Product research**.”
|
||||
|
||||
The success card keeps the raw tool result collapsed. Expanding it displays
|
||||
formatted JSON. The latest five-journey browser suite passed in 1.7 minutes and
|
||||
checks separate source/reply run IDs, readable output, and result expansion.
|
||||
This is real Codex execution against a local fixture, not live Notion evidence.
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
# Connection reviews in task history
|
||||
|
||||
An agent call governed by **Ask human / Ask first** creates a server-owned
|
||||
`request_confirmation.payload.toolAction` interaction linked to the existing
|
||||
`tool_action_requests` record. No provider call runs while this review is pending.
|
||||
The default approval lifetime remains one hour.
|
||||
|
||||
The task feed keeps one record for each review. **Review request** opens the
|
||||
composer takeover. Dismissing the takeover only hides it; the review remains
|
||||
pending, ordinary comments do not supersede it, and the agent is not resumed.
|
||||
Multiple reviews retain separate records and the existing takeover navigation.
|
||||
The card shows the app icon and a short request description. Destructive actions
|
||||
retain destructive approval styling. Audit metadata and signed arguments remain
|
||||
on the underlying review record.
|
||||
|
||||
**Approve & run** executes the signed, stored arguments once. **Decline** executes
|
||||
nothing in one click. Both the task and Connections queue use
|
||||
one decision transaction; a decision removes the pending queue item while its
|
||||
history remains on the task. The human decision, resolver, remembered permission,
|
||||
and execution outcome remain separate. Provider failure does not turn an approved
|
||||
decision into a decline. Successful results stay collapsed behind the status chevron;
|
||||
expanding it shows formatted JSON (or plain text). The resumed agent processes the
|
||||
recorded result in a new turn and writes the user-facing answer. Live activity
|
||||
invalidation refreshes both surfaces, with existing polling/reconnect reconciliation
|
||||
retained.
|
||||
|
||||
## Remembered permission
|
||||
|
||||
Choose **Always allow** from the split button beside **Approve & run**. It
|
||||
atomically saves approval and an action-wide trust rule for the
|
||||
same agent, connection, and action, restricted to the originating project when one
|
||||
exists. Future argument values may differ. The menu item exposes the scope through
|
||||
its tooltip and accessible description; the receipt records the saved permission. If saving the rule
|
||||
fails, the approval transaction rolls back and no provider call runs.
|
||||
|
||||
The rule remains bound to the reviewed catalog definition/schema. Changed
|
||||
definitions require review again. Revocation, explicit denial, connection access,
|
||||
and formal approval requirements remain effective. Manage/revoke rules through the
|
||||
existing Connections trust-rule controls.
|
||||
|
||||
The accept/approve endpoints support optional `rememberAction: true`; omission
|
||||
continues to approve once. Trust-rule promotion supports `argumentMode: "action"`;
|
||||
its existing omitted/`"exact"` mode continues to bind exact argument values.
|
||||
|
||||
## Governed waiting and recovery
|
||||
|
||||
The gateway returns `approval_required` with the linked request/interaction IDs
|
||||
and instructions to finish unrelated work, then yield `in_review` without retrying
|
||||
or claiming completion. Agent task completion is rejected while a linked action
|
||||
is pending, approved, or executing. Provider execution is server-owned.
|
||||
|
||||
`tool_action_deliveries` is a durable, content-free outbox keyed by action request.
|
||||
It refers to the authoritative request, invocation, and interaction instead of
|
||||
copying provider data. Once the originating runs have ended and no other task
|
||||
interactions remain pending, ready outcomes are batched into one continuation
|
||||
wake. The wake includes the recorded result/decline and instructions not to repeat
|
||||
the operation. Native runners materialize validated server-owned interaction
|
||||
outcomes; legacy runners receive the wake context and agent message. Existing
|
||||
scheduler eligibility and budget gates still apply. Closed tasks retire receipts;
|
||||
reassignment does not deliver the old agent's outcome to another agent.
|
||||
|
||||
Startup and periodic sweeps recover committed approvals, undelivered outcomes,
|
||||
expiry, and incomplete feed projections. An execution left in progress for ten
|
||||
minutes is marked failed with `tool_execution_outcome_unknown`. Its external
|
||||
outcome is uncertain: inspect the provider before retrying. It is never
|
||||
automatically replayed. This grace period exceeds the current approved-call timeout.
|
||||
|
||||
Migration 0249 adds the outbox and a partial unique wake-idempotency index.
|
||||
The index is built transactionally; migration can briefly block wake-table writes
|
||||
while PostgreSQL scans an existing large table. No external payload is added to the
|
||||
outbox.
|
||||
|
||||
## Verification workflows
|
||||
|
||||
Run the credential-free, isolated browser suite:
|
||||
|
||||
```sh
|
||||
PAPERCLIP_E2E_PORT=3222 pnpm exec playwright test -c tests/e2e/connection-reviews.config.ts
|
||||
```
|
||||
|
||||
This starts a dedicated embedded database/server and local MCP fixture, configures
|
||||
Ask first through the UI, and verifies approve, decline, remembered permission with
|
||||
changed arguments, dismissal/reopening, ordinary comments, cross-tab queue/task
|
||||
updates, provider failure, and restart while waiting. Assertions include useful
|
||||
agent results and provider invocation counts. Screenshots, JSON journey identifiers,
|
||||
and traces are attached to the Playwright HTML report. The deterministic agent is a
|
||||
scripted process adapter; these results do not prove model-runner behavior.
|
||||
|
||||
Run the opt-in, local model-runner matrix with the harness's normal credentials:
|
||||
|
||||
```sh
|
||||
PAPERCLIP_RUNNER_E2E_CONNECTION_REVIEWS=1 pnpm test:e2e:runner -- --suite connection-reviews
|
||||
```
|
||||
|
||||
The 16 cells cover approve, decline, always allow, and restart/resume for native
|
||||
Codex, native ACPX Claude, legacy Codex CLI, and legacy Claude CLI. Qualified model
|
||||
settings come from the existing runner catalog. This flag does not expand the
|
||||
normal hosted/Daytona matrix. The fixture MCP provider is real HTTP but is not
|
||||
Notion; live Notion evidence must be reported separately.
|
||||
|
||||
For a real Notion test, start `paperclipai test-drive` from this checkout using
|
||||
valid provider credentials, verify its process checkout/port ownership, connect
|
||||
Notion normally, set an available read-only search/list action to Ask human, and
|
||||
perform the same approve/decline/always-allow journeys for all four profiles.
|
||||
Capture request/run IDs, screenshots, traces, and actual page results. Missing
|
||||
credentials or account/provider access are untested dependencies, never a pass.
|
||||
|
||||
## Storybook
|
||||
|
||||
```sh
|
||||
pnpm --filter @paperclipai/ui exec storybook dev -p 6018 -c storybook/.storybook --no-open --ci
|
||||
```
|
||||
|
||||
Open **Chat & Comments / Connection Reviews**. The production task thread/card and
|
||||
Connections queue cover pending, dismissed/reopened, multiple requests, each
|
||||
submitting action, recoverable errors, concurrent resolution, approved/executing,
|
||||
success/failure, decline with/without a reason, expiry/cancellation, remembered
|
||||
scope/receipt, approval options, narrow layout, and queue/empty states. The global
|
||||
theme toolbar switches light/dark. Story actions simulate server responses; use the
|
||||
browser suite for integration proof.
|
||||
|
||||
Provider output, execution errors, and review notes travel in the continuation's
|
||||
`untrustedToolResults` field, separate from its control instructions. Both native
|
||||
and legacy wake prompts render those fields as fenced JSON with an explicit
|
||||
untrusted-data boundary. Embedded provider instructions cannot grant permission
|
||||
or change the task's continuation policy. Wake materialization redacts secrets
|
||||
and bounds each text field before rendering.
|
||||
|
||||
A continuation includes at most eight shortened result records and caps the
|
||||
serialized wake context at 32 KB. It links to the task interaction API for all
|
||||
full outcomes and instructs the agent to retrieve omitted or incomplete results
|
||||
before finishing. A committed receipt cutoff preserves acknowledgement of that
|
||||
referenced set across restart, without putting an unbounded ID list in the wake.
|
||||
Task review queries reconcile every 20 seconds if a live event is missed.
|
||||
|
|
@ -6,6 +6,8 @@ Run scope: `ui/src/components/` and `ui/src/pages/` on branch `design/token-extr
|
|||
|
||||
## Counts
|
||||
|
||||
Execution recovery reuses the existing transcript header and task status. Routine phases add no list badges, status cards, or reconciliation dialogs. Only a transient reconnection changes the header text. Automatic recovery decisions remain in the local run log. Storybook **Tasks / Execution recovery** demonstrates quiet task lists, native and legacy transcript headers, and dashboard composition.
|
||||
|
||||
| Area | Count |
|
||||
|---|---:|
|
||||
| Shared primitives (`ui/src/components/ui/`) | 24 |
|
||||
|
|
@ -404,3 +406,16 @@ Per-component rationale:
|
|||
### 7.3 Interactive-card affordance (Run 3 review feedback)
|
||||
|
||||
`Card` gained an `interactive` prop — pointer cursor, quiet hover (border→foreground/20 + shadow-md lift), focus-visible ring — used when the whole card is a click target (e.g. Companies selector). Skills tiles (CompanySkills `SkillCard`) and artifact cards (`ArtifactCard`/`ArtifactGroupCard`) apply the same recipe verbatim since they cannot render through Card (button/Link semantics). Static container Cards stay affordance-free by design.
|
||||
|
||||
|
||||
## In-task connections — 2026-09-07
|
||||
|
||||
| Reusable surface | Production owner | Hosts / coverage |
|
||||
|---|---|---|
|
||||
| Connection request card | `ui/src/features/connections/ConnectionIntentInteractionBody.tsx` | Task timeline, interaction card, design guide; pending, reuse, authorizing, retry, resolved, audience and error stories |
|
||||
| Connection setup flow | `ui/src/features/connections/ConnectionSetupFlow.tsx` | Connections page and task dialog share provider forms, OAuth, validation and additive installs |
|
||||
| OAuth handoff | `OAuthConnectStateScreen` in the shared setup module | Entry, starting, open window, blocked popup, closure, callback failure, retry and new-tab fallback |
|
||||
| Identity and agent access | `AccessStep` in the shared setup module | Personal, organization, dedicated agent, unavailable identity and loading; task host fixes install reach to the requester |
|
||||
| Setup completion | `ConnectionSetupCompletionScreen` in the shared setup module | Page and dialog; identity, granted agent access and enabled actions |
|
||||
|
||||
Independently addressable examples live under `Connections/In-task connections` in Storybook. The task composer remains available while a card is pending. These components use the existing token and primitive layers.
|
||||
|
|
|
|||
|
|
@ -12,22 +12,86 @@ Delegated work and interactions persist their originating context. Retries retai
|
|||
|
||||
## Managed GitHub operations
|
||||
|
||||
New executions receive token-free `git` and `gh` launchers and a run-scoped capability. Each launcher invocation requests the active context through the authenticated runtime transport and resolves one eligible credential at operation start. A `gh` command's child Git processes inherit that command's captured identity. Later steering does not change already-started operations. When a subsequent run resumes a settled native conversation, the controller starts a fresh provider process with that run’s capability and rebinds its token-free launcher paths. The durable conversation and protected provider settings remain unchanged. Local and remote durable runners complete their bounded suspension before the controller releases the session for the next run, so a queued continuation cannot race unfinished cleanup.
|
||||
Executions with managed GitHub configured receive token-free `git` and `gh` launchers and a run-scoped capability. Each launcher invocation requests the active context through the authenticated runtime transport and resolves one eligible credential at operation start. A `gh` command's child Git processes inherit that command's captured identity. Later steering does not change already-started operations. When a subsequent run resumes a settled native conversation, the controller starts a fresh provider process with that run’s capability and rebinds its token-free launcher paths. The durable conversation and protected provider settings remain unchanged. Local and remote durable runners complete their bounded suspension before the controller releases the session for the next run, so a queued continuation cannot race unfinished cleanup.
|
||||
|
||||
The broker endpoint rejects browser origins and session cookies, validates a distinct signed runtime scope, and rechecks the company, agent, and live run. Sandboxes relay the capability through the existing authenticated callback bridge. Tokens are returned only to the managed command process. They are not persisted in identity history or injected into the long-lived provider process.
|
||||
|
||||
Low-trust executions cannot receive raw GitHub credentials, including dedicated
|
||||
agent tokens. The broker rechecks current agent, project, task, and retained run
|
||||
policies before credential resolution. An external guest's internal sponsor is
|
||||
accountable for the task, but does not authorize using the sponsor's account.
|
||||
Read-only access must use separately authorized tools that enforce that boundary.
|
||||
|
||||
Server-side Git operations and GitHub gateway calls follow the same selection rules. Approved gateway operations retain their signed originating identity. Connection audience and tool policies continue to apply to the selected person's connection. Native catalogs remain stable across identity changes, but each invocation resolves the selected grant again. Personal OAuth secret declarations survive connection pauses and metadata edits.
|
||||
|
||||
Managed commands disable ambient Git credential helpers, Git global/system configuration, host GitHub CLI configuration, and host SSH identity access. Per-operation GitHub CLI configuration is isolated in a writable configuration directory beneath the managed launcher directory. Missing credentials clear previous author and token values; no teammate, standing delegation, host token, or company-default user's account is substituted. Anonymous/local operations remain available where supported.
|
||||
|
||||
Remote launchers prepend their directory to the execution target's effective
|
||||
`PATH`. An explicit remote `PATH` override is preserved; otherwise Paperclip
|
||||
reads the provider's environment before staging the launcher shell files.
|
||||
This keeps legacy NVM and user-local agent installations available alongside
|
||||
newer images with system-wide CLIs. The generated shell files retain that
|
||||
combined path with managed `git` and `gh` first. Sandbox command checks use
|
||||
the same sanitized environment as execution, so a CLI visible only in the
|
||||
provider's default environment cannot pass the launch check. Failed path
|
||||
discovery stops startup instead of silently falling back to a minimal path.
|
||||
|
||||
Scripts that previously read a persistent `GH_TOKEN` must use managed `git`, `gh`, or GitHub gateway tools. Managed execution skips legacy GitHub token bindings in agent, environment, project, and routine configuration before secret preflight. Configure personal or dedicated access through the GitHub connection instead. Directly invoking an unmanaged executable or retaining a token obtained during an earlier invocation is outside the managed invocation contract.
|
||||
|
||||
## Legacy hosts and networking
|
||||
|
||||
When no managed GitHub connection is installed for an agent, standard-trust
|
||||
local and SSH executions retain that execution host's existing Git and GitHub
|
||||
CLI credentials, configuration, credential helpers, and SSH agent. Paperclip
|
||||
does not import controller credentials into an SSH target. Sandbox, plugin,
|
||||
and low-trust executions do not receive this compatibility fallback. Once a
|
||||
managed connection is configured, unavailable or revoked access never falls
|
||||
back to host authentication. Switching modes replaces the provider process
|
||||
while preserving the settled conversation.
|
||||
|
||||
Runner network access is independent of GitHub credentials. The controller
|
||||
enables networking for standard-trust execution. Low-trust runs and runners
|
||||
without a controller network decision retain a restricted default. An operator
|
||||
can set `PAPERCLIP_RUNNER_NETWORK_ACCESS=disabled` to restrict normal execution;
|
||||
user environment bindings cannot override that decision. Outer execution-
|
||||
environment network restrictions still apply. The controller projects the assigned worktree's Git metadata paths so
|
||||
Git can operate without exposing unrelated workspace or provider state. The
|
||||
sandbox also receives read access to validated provider executable resources
|
||||
and the target host's DNS and CA files, including resolver symlink targets
|
||||
outside `/etc`. Provider credential directories remain isolated.
|
||||
|
||||
A managed broker outage does not prevent local Git operations. Launchers clear
|
||||
credentials and run the command without authentication, with a redacted error
|
||||
category identifying configuration setup, transport, or capability rejection.
|
||||
They do not retain a previous operation's token or replay a GitHub operation.
|
||||
|
||||
Healthy eligible grants for the same stable GitHub account ID take precedence
|
||||
over duplicates with failed health checks. Credential acquisition can retry
|
||||
once against another grant for that same principal and account, before any
|
||||
GitHub operation begins. Run identity diagnostics include the selected
|
||||
connection and grant IDs, without credential values. Access-refresh conflicts
|
||||
retry once against current state and never turn a concurrency conflict into
|
||||
a reconnect requirement.
|
||||
|
||||
## Dedicated accounts and diagnostics
|
||||
|
||||
An explicit dedicated-agent grant overrides personal selection. Revoked, disabled, unavailable, or ambiguous dedicated grants do not fall back to a person's account. Removing the dedicated configuration restores personal selection.
|
||||
|
||||
Connection setup and permissions display: “This agent uses this GitHub account for everyone's work, instead of the person giving instructions.”
|
||||
|
||||
The GitHub permissions page shows repositories across all connected accounts in one scrollable list. It has no account filter or repository search. Repository icons, private-repository indicators, refresh, and GitHub configuration links remain available. The “Add More Repos on GitHub” button opens GitHub’s app installation and repository-access setup.
|
||||
|
||||
Multiple eligible connections for the same GitHub account are treated as one
|
||||
identity, using GitHub's stable account ID rather than its login. The resolver
|
||||
selects an available grant, preferring the newest authorization with a stable
|
||||
ID tie-breaker. Duplicate eligibility includes an active credential record with
|
||||
the correct owner, the OAuth access-token reference, and repository access
|
||||
metadata. It keeps that grant's credential and connection policy together;
|
||||
it does not combine repository access or bypass connection audiences. Distinct
|
||||
accounts or unidentifiable duplicate grants remain ambiguous. Managed commands
|
||||
print the redacted reason when GitHub access is unavailable, while unrelated
|
||||
local operations can still proceed without credentials.
|
||||
|
||||
Run details show identity revisions and redacted GitHub results: responsible person, selected login when available, personal/dedicated source, and an unavailable reason. Tasks do not receive an additional identity indicator or takeover action.
|
||||
|
||||
## Deployment and verification
|
||||
|
|
|
|||
|
|
@ -144,6 +144,16 @@ The active-lock lifecycle is part of the checkout contract:
|
|||
|
||||
Stale-lock recovery is crash recovery, not a retry loop. Paperclip must not clear or adopt locks held by non-terminal runs. After stale cleanup, a checkout `409` should mean a real live owner, status/assignee mismatch, unresolved blocker, or active gate still prevents checkout. Agents must treat that `409` as an ownership conflict and stop rather than retrying the same checkout.
|
||||
|
||||
### Known execution waits at admission
|
||||
|
||||
A known execution hold is a waiting condition, not a new execution attempt. Every issue wake must read the current effective reconciliation hold under the issue admission lock before creating a run. Resolved recovery bookkeeping can still carry a no-replay hold; only clearing the effective hold makes admission eligible again. The final dispatch gate remains required for changes after admission.
|
||||
|
||||
Repeated automatic signals for an unchanged gate share one durable skipped-wake diagnostic, scoped to company, agent, issue, gate code, and condition identity. The diagnostic retains the first request and counts later observations. This applies to execution reconciliation, dependencies, pause holds, company and agent availability, budget blocks, and disabled heartbeats. These diagnostics do not consume provider attempts and are never proof that a future wake was delivered. All current gates are checked again on the next wake, including the periodic dependency reconciliation sweep. Clearing one gate does not bypass another.
|
||||
|
||||
New comments received during an execution hold retain their individual deferred receipts and ordered comment ids. Release cannot drain those receipts while replay remains blocked; the next eligible wake can adopt them. Authorized external-chat requests also remain deferred with their exact durable receipt. They must use normal promotion and current authorization; a generic wake cannot adopt only their comment ids and discard their actor or session contract. A wait does not authorize replay, reset an incident retry budget, or bypass an interaction's delivery rules.
|
||||
|
||||
The conversation groups repeated empty pre-start reconciliation cancellations into a neutral waiting notice. Started runs, actual startup failures, and run history remain inspectable. No historical run records are deleted.
|
||||
|
||||
### Pre-dispatch configuration validation
|
||||
|
||||
Pre-dispatch configuration validation is a distinct gate that runs after ownership and checkout are resolved but before the control plane actually dispatches a run.
|
||||
|
|
@ -471,7 +481,7 @@ Agent-assigned `in_review` with no typed participant is only healthy when one of
|
|||
|
||||
An `in_review` issue is stalled when it has no typed participant, no pending interaction or approval, no user owner, no active monitor, no active run, no queued wake, and no explicit recovery action. Paperclip should surface that state as recovery work rather than silently completing the issue or leaving blocker chains parked indefinitely.
|
||||
|
||||
When an execution-policy review stage has a pending agent participant, the participant's run is part of the review path only while it is live or queued. If that participant run reaches a terminal state while `executionState.status` remains `pending`, no decision has been recorded. Paperclip should queue one bounded normal-model recovery wake for the same participant when the agent is invokable and no other review path exists. If that recovery run also finishes while the stage remains pending, or the participant cannot be invoked, Paperclip must move the source issue to an explicit blocked/recovery path instead of leaving `in_review` to drift silently.
|
||||
When an execution-policy review stage has a pending agent participant, the participant's run is part of the review path only while it is live or queued. If that participant run reaches a terminal state while `executionState.status` remains `pending`, no decision has been recorded. After a successful run with no review decision, Paperclip should queue one bounded normal-model recovery wake for the same participant when the agent is invokable and no other review path exists. A failed participant instead follows the provider-continuity rules below: local conversational adapters can start a bounded continuation turn, while native sessions use validated resume/replacement. Other adapters retain their action-recovery gates. The original assignee stays unchanged. If that recovery run also finishes while the stage remains pending, or the participant cannot be invoked, Paperclip must move the source issue to an explicit blocked/recovery path instead of leaving `in_review` to drift silently.
|
||||
|
||||
### Issue monitors
|
||||
|
||||
|
|
@ -552,6 +562,8 @@ Recovery rule:
|
|||
|
||||
This is an active-work continuity recovery.
|
||||
|
||||
After a productive successful run, recovery checks that the issue is still `in_progress` and assigned to the same agent under the enqueue transaction's issue lock. The sweep's earlier snapshot cannot authorize a continuation after completion, cancellation, reassignment, or a move to another status. A mismatch records a skipped wake receipt without creating a run. An empty queued continuation cancelled because the issue became terminal is omitted from task chat; its cancellation remains in the run log. Runs that actually started still show their stop state.
|
||||
|
||||
The same bounded rule applies when the previous heartbeat reported waiting on a local/background watcher and that watcher was killed, disappeared, or was never represented by a durable Paperclip primitive. Paperclip queues at most one continuation for the same recovery fingerprint. If the continuation also leaves only local watcher evidence, Paperclip must surface a real blocker or explicit recovery action instead of repeating continuation recovery. A new monitor, scheduled wake, healthy delegated blocker issue, or other durable source mutation resolves that recovery fingerprint normally.
|
||||
|
||||
#### Deliberate wait is not a lost run
|
||||
|
|
@ -779,12 +791,98 @@ Auto-recovery is allowed when ownership is clear and the control plane only lost
|
|||
|
||||
Examples:
|
||||
|
||||
- requeue one dispatch wake for an assigned `todo` issue whose latest run failed, timed out, or was cancelled
|
||||
- requeue one continuation wake for an assigned `in_progress` issue whose live execution path disappeared
|
||||
- requeue one dispatch wake for an assigned `todo` issue whose latest run failed, timed out, or was cancelled under the bounded conversation or provider-continuity rules below
|
||||
- requeue one continuation wake for an assigned `in_progress` issue whose live execution path disappeared under the bounded conversation or provider-continuity rules below
|
||||
- assign an orphan blocker back to its creator when that blocker is already preventing other work
|
||||
|
||||
Auto-recovery preserves the existing owner. It does not choose a replacement agent.
|
||||
|
||||
### Completion tools and final answers
|
||||
|
||||
A completion tool such as `paperclip_finish` reports task disposition; it does not
|
||||
end the provider turn. Paperclip continues persisting and displaying provider
|
||||
events until an authoritative turn terminal arrives. The completion report starts
|
||||
no interruption timer. Existing execution timeouts, cancellation, governed waits,
|
||||
and active-goal rules still apply. A later failed or cancelled terminal remains
|
||||
failed or cancelled even when the agent already reported completed work.
|
||||
|
||||
The final assistant message is the visible task response. Response selection runs
|
||||
after preceding event persistence completes; the completion summary cannot replace
|
||||
an available final answer. Existing fallback and explicit-comment precedence still
|
||||
apply. Stream closure without a turn terminal is not proof of success. Event
|
||||
replay uses the existing source receipts and never repeats provider work merely
|
||||
to recover recorded output.
|
||||
|
||||
### Provider continuity and bounded finalization
|
||||
|
||||
A permanently unusable native runner session may be replaced only with evidence that its predecessor is stopped and fenced, completed results and workspace state are preserved, required task history is available, and pending effects have been reconciled. A provider-native shell command or external write without a reliable outcome receipt is unknown. Unknown effects, integrity failures, and unverified process ownership never authorize speculative replay. Once automatic recovery is ruled out, Paperclip selects a conservative default: preserve recorded work, stop the affected task, and retain a durable no-replay hold. Unknown action outcomes remain unknown. No reconciliation form or user diagnosis is required.
|
||||
|
||||
Bootstrap retries, exact-checkpoint resumes, and fresh replacement sessions share three total provider attempts, including the original attempt. Linked run IDs, controller restarts, and duplicate wakes do not reset this budget. Automatic attempts retain the 30-second delay. Replacement scheduling and predecessor lineage commit together, with one successor per predecessor and admission through the normal task locks, authorization, pause, approval, and budget gates.
|
||||
|
||||
Provider execution and control-plane finalization have different clocks. A healthy provider can think or execute a long tool without output. Once execution settles, recovery and finalization control steps have a 60-second deadline, checked on startup and every 15 seconds. With a healthy database and scheduler, an abandoned transition must be repaired or surfaced within 90 seconds. Terminal persistence must not wait on provider cleanup or publication; a late finalizer cannot change a reassigned or closed task or release another run's locks. Historical ambiguous runs are never automatically replayed after an upgrade.
|
||||
|
||||
Every continuation carries the triggering request, ordered user direction, interaction outcomes, completed work, and explicit history coverage. A delivered message remains part of the task's request after its connection or approval resolves. The original title is background; a completed Notion read does not satisfy a later Gmail request. Author and source-trust boundaries survive rendering into both native and legacy prompts. Missing required history must be fetched before dispatch rather than described as complete.
|
||||
|
||||
### Interrupted conversation continuation
|
||||
|
||||
An interrupted conversation does not permanently block its task. For local conversational adapters, Paperclip starts a new bounded turn with the existing session when compatible, or the full task conversation when the session is unavailable. The prompt says: “Your previous run was interrupted. Continue from where you left off.” The agent decides what remains from the history and latest user request. Paperclip never automatically replays recorded tool calls. Unknown past action outcomes are not a task-wide execution gate, and no action-reconciliation questionnaire is required.
|
||||
|
||||
Shutdown, process loss, and provider failure use the existing durable failure retry counter and delay. Ordinary failure recovery permits at most two automatic retries in a failure chain. Accepted-interaction infrastructure recovery retains its existing bounded policy. Repeated scheduler visits reuse the same successor; restarting the server does not reset the counter. After exhaustion, automatic attempts stop. A new explicit user message can start a fresh run and failure budget. Productive max-turn continuation and confirmed workspace waits keep their separate existing semantics.
|
||||
|
||||
Real gates still apply: company and task ownership, active provider ownership, budget limits, agent availability, dependencies, pending approval/review paths, and explicit pause holds. Native runner reattachment and finalization retain their existing ownership protocol. Process, HTTP, and gateway adapters retain their recovery rules because invoking those adapters can itself repeat an external action rather than start a conversation turn.
|
||||
|
||||
An operator Stop still waits for local provider termination. Stop alone never promotes deferred comments or starts an automatic continuation. Once stopped, the next explicit wake adopts pending comment IDs in order through the existing queue. A compatible saved ACP session can resume, and an unavailable or incompatible session can start fresh with the full task context. Run credentials and scratch paths remain scoped to the new run. A subtree pause requires Resume; a message does not bypass it.
|
||||
|
||||
Historical legacy interruption holds for conversational adapters no longer block new messages or Resume. Classification uses the run’s saved adapter invocation or continuation policy, never the agent’s current adapter settings. Missing historical adapter evidence retains the hold. A terminal row with a live predecessor process or unreleased environment lease still blocks actual admission and Resume. Retry scheduling can happen before cleanup, but grants no execution authority. Recovery folds their obsolete no-replay bookkeeping without changing task ownership, status, or automatically waking old work. The audit trail remains readable. Native integrity and ownership holds, and non-conversational adapter holds, remain enforced.
|
||||
|
||||
The server projection remains available for diagnostics. Normal working, finishing, and interaction waits add no badges or cards to task lists or feeds. Active transcript headers keep saying Working during automatic retry and execution confirmation; attempts, causes, and recovery decisions belong in the run log. Recovery uses the existing transcript and run log rather than adding a reconciliation form. A cancelled run that never started says “Couldn't start” instead of implying that the agent answered.
|
||||
|
||||
### Codex startup and provider state
|
||||
|
||||
Paperclip trusts the server-selected startup execution root in the isolated
|
||||
Codex configuration. Resolve that root on the execution host, including the
|
||||
main repository trust key for Git worktrees. Start the provider in that same
|
||||
root. This does not change sandbox permissions, tool authorization, secret
|
||||
access, or Codex's separate per-hook trust policy.
|
||||
|
||||
Codex retains the model conversation. Paperclip resumes with `excludeTurns: true`,
|
||||
reads lightweight thread state, and fetches paginated turn metadata or specific
|
||||
turn items only when execution reconciliation needs them. Unsupported
|
||||
or incomplete history is an explicit error, not evidence of idle execution.
|
||||
|
||||
The root-thread usage snapshot sent during resume belongs to its reported
|
||||
completed turn. Retain a bounded local diagnostic and use cumulative totals as
|
||||
a baseline; do not emit a warning or charge its historical `last` usage to the
|
||||
new run. Preserve the baseline across recovery of the same run and start a new
|
||||
delta when attaching a new run. Other stale-event and authority checks remain.
|
||||
|
||||
|
||||
### Explicit user continuation after a native failure
|
||||
|
||||
An execution recovery hold blocks automatic replay. A new authenticated user
|
||||
comment can authorize a fresh native conversation turn after the predecessor's
|
||||
execution is confirmed stopped. This is a new request, not another automatic
|
||||
attempt in the failed incident. The old attempt count and unknown action outcomes
|
||||
remain unchanged.
|
||||
|
||||
Admission validates the persisted comment's author, task, and time against every
|
||||
held predecessor. An agent-authored comment, an old queued request, or a generic
|
||||
system wake cannot release a hold. The source task keeps its assignee. Process
|
||||
ownership, active controllers, cleanup leases, pause, approval, budget, and normal
|
||||
execution gates still apply. Dependency-blocked interaction mode remains limited
|
||||
to its existing answer/triage contract.
|
||||
|
||||
The hold retirement, audit record, and new run commit together under the task
|
||||
lock. The new turn uses a fresh provider session and retains the latest user
|
||||
request, task history, completed work, and the interruption notice. It receives
|
||||
no instruction to repeat old tool calls. Later messages cannot reset the old
|
||||
incident's retry budget or create another automatic replacement for it.
|
||||
|
||||
The initial native admission path verifies local process identities. Missing
|
||||
process identity or remote ownership without a target-aware stop proof remains a
|
||||
hold; a terminal database status or a PID check on the wrong host is insufficient.
|
||||
No historical task is automatically awakened by this change.
|
||||
|
||||
### Explicit Recovery Action
|
||||
|
||||
Paperclip opens an explicit recovery action when the system can identify a problem but cannot safely complete the work itself.
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ task.run
|
|||
│ ├── environment.startup
|
||||
│ │ ├── environment.acquire
|
||||
│ │ └── environment.workspace.realize
|
||||
│ ├── skills.prepare
|
||||
│ ├── heartbeat.prepare_before_environment
|
||||
│ ├── heartbeat.prepare_after_environment
|
||||
│ └── native.coordinator.claim
|
||||
|
|
@ -753,16 +754,93 @@ To add a name or an enum value, extend the literal constant in
|
|||
|
||||
### Known behavior: aggregate retained body bytes
|
||||
|
||||
The HTTP/2 bridge bounds retained body bytes for one route only. Each route
|
||||
holds up to 8,388,608 bytes (8 MiB) at its own peak (see
|
||||
`HTTP2_BRIDGE_MAX_CONCURRENT_STREAMS` in `http2-bridge-server.ts`). The host
|
||||
process admits up to 128 concurrent routes (see
|
||||
`DEFAULT_MAX_CONCURRENT_DUPLEX_ROUTES` in `plugin-worker-manager.ts`). The
|
||||
process can therefore retain up to 1,073,741,824 bytes (1 GiB) of body data
|
||||
across every route at the same time.
|
||||
Each HTTP/2 bridge route holds up to 168,820,736 bytes (161 MiB) at its own
|
||||
peak (see `HTTP2_BRIDGE_MAX_CONCURRENT_STREAMS` in `http2-bridge-server.ts`).
|
||||
The host process admits up to 128 concurrent routes (see
|
||||
`DEFAULT_MAX_CONCURRENT_DUPLEX_ROUTES` in `plugin-worker-manager.ts`). Those
|
||||
two figures alone would let the process retain up to 21,609,054,208 bytes
|
||||
(about 20.1 GiB) of body data across every route at the same time.
|
||||
|
||||
The process does not reach that figure, on two levels.
|
||||
`HTTP2_BRIDGE_MAX_PROCESS_BODY_BYTES` (`http2-bridge-server.ts`) enforces a
|
||||
real, live ledger: 1,073,741,824 bytes (1 GiB) across every route, not merely
|
||||
an accepted paper ceiling. Every HTTP/2 stream creates one `BridgeBodyReservation` owner over
|
||||
its lifetime, and every source-level full-body buffer that stream retains —
|
||||
its request-body chunk array, the concatenated request body, the
|
||||
response-body chunk array, and the concatenated response body — reserves
|
||||
against that one owner before it allocates. A reservation that would pass the
|
||||
process total is denied before it copies anything, and the host answers 503
|
||||
instead of accepting the body. The reservation stays live for the response
|
||||
body until the HTTP/2 write actually finishes flowing to the peer or the
|
||||
stream closes, not merely until the write call returns, so a slow or
|
||||
backpressured peer cannot hold response bytes in memory the ledger no longer
|
||||
counts.
|
||||
|
||||
`HTTP2_BRIDGE_MAX_ROUTE_BODY_BYTES` adds a second, per-route ledger on top of
|
||||
that process-wide one: each route's own reservations also check a ceiling
|
||||
scoped to that one route (its own 168,820,736-byte peak from above), so one
|
||||
busy or malicious route can pass its own ceiling and get denied with a 503,
|
||||
but it can never spend the whole process-wide total and deny every sibling
|
||||
route admission. This accounting covers source-level full-body buffers only:
|
||||
internal Node.js and Undici copies (socket buffers, HTTP/2 frame buffers,
|
||||
decompression buffers) stay outside it.
|
||||
|
||||
The generated gateway process inside the sandbox (`getSandboxCallbackBridgeServerSource`
|
||||
in `sandbox-callback-bridge.ts`) enforces its own separate ledger, independent
|
||||
of the two host-side ledgers above: each side bounds only the memory in its
|
||||
own process. `readBodyBytes` reserves a request body's chunk bytes as they
|
||||
arrive, then reserves the concatenated buffer's own byte count before
|
||||
`Buffer.concat` allocates it, against a ceiling of `maxBodyBytes * 8` (4
|
||||
concurrent bodies, each counted twice for its two live copies). A denied
|
||||
reservation answers 503 with no forward call. Each request handler releases
|
||||
its own reservation once the whole request settles: a completed response, a
|
||||
thrown error, a client abort, or a deadline timeout all reach the same
|
||||
release call.
|
||||
|
||||
This is accepted, known behavior. The process tracks no aggregate byte
|
||||
ledger across routes: a per-route bound stops one busy route from starving
|
||||
another route's own budget, but the host enforces no smaller ceiling on the
|
||||
sum across every route.
|
||||
Keep every dimension low-cardinality and free of user content.
|
||||
|
||||
### Shared skill preparation
|
||||
|
||||
`skills.prepare` measures the shared inventory listing and runtime materialization
|
||||
inside `task.prepare`. It is also contained in the broader
|
||||
`heartbeat.prepare_before_environment` interval; do not add those two durations.
|
||||
Preparation failures emit a failed span even when no native session starts.
|
||||
It carries no skill contents, identifiers, locations, or credentials. It uses the
|
||||
existing run performance events and operator-configured OpenTelemetry endpoint;
|
||||
no first-party Telemetry event is added.
|
||||
|
||||
Runtime preparation refreshes the company inventory once per listing. Local and
|
||||
catalog directories remain direct sources, so edits are visible on the next
|
||||
preparation. Explicit version selections still use their stored snapshots.
|
||||
|
||||
Reconstructed skills use `__runtime_cache_v1__/<skill-id>/<fingerprint>/files`
|
||||
beneath company skill storage, with a sibling manifest of paths, sizes, and SHA-256
|
||||
content digests. Every warm hit validates the manifest and exact file contents;
|
||||
it does not fetch upstream, rewrite files, or remove directories. The fingerprint
|
||||
includes installed source identity, revision, file inventory, and stored Markdown,
|
||||
and excludes display names, stars, and general update timestamps. Manifests stay
|
||||
outside the directory delivered to agents.
|
||||
|
||||
GitHub and skills.sh imports are cached only when pinned to a full commit SHA.
|
||||
Remote freshness is explicit: update or reimport selects a new revision, including
|
||||
supporting-file-only changes. A branch advancing upstream does not change an
|
||||
installed revision. Legacy mutable refs retain uncached behavior until updated.
|
||||
URL-only skills use stored Markdown. An unavailable new revision reports missing;
|
||||
it never silently reuses an older revision. Stored `SKILL.md` remains a fallback,
|
||||
but missing supporting files prevent publication of a reusable partial cache.
|
||||
|
||||
Builds publish read-only files and directories from unique staging directories.
|
||||
A skill-scoped lock serializes builds and cleanup across processes. Cold builders
|
||||
recheck that the skill still exists under its original key before reading files
|
||||
and before atomic publication. Existing valid
|
||||
revisions stay readable during updates. Invalid entries are quarantined in the
|
||||
same skill cache root for inspection; rename/removal cleans up that skill's cache.
|
||||
Read-only listings validate caches without downloading or repairing them. A
|
||||
publication lock left by an abruptly terminated process is reported for operator
|
||||
cleanup; remove it only after confirming its recorded PID is no longer running.
|
||||
|
||||
Run `pnpm --filter @paperclipai/server exec tsx ../scripts/benchmark-skill-preparation.ts` for an isolated embedded
|
||||
PostgreSQL benchmark with 114 mixed skills and at least 400 remote files. It
|
||||
reports one cold sample and ten warm samples (one in a new process), refresh and
|
||||
fetch counts, rebuilds, missing entries, and content checks. Upstream responses are
|
||||
deterministic fixtures; use real deployed run spans for user-facing latency.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
# Reliable execution and continuation — implementation and verification
|
||||
|
||||
## Outcome
|
||||
|
||||
Failed provider sessions retain their structured failure meaning. Recovery uses a shared incident budget of three provider attempts, including the original attempt. A fresh replacement requires a fenced predecessor, durable completed results, preserved workspace state, authorized task history, and reconciled pending effects. Unknown external effects receive an automatic preserve-without-replay disposition; they never require a reconciliation form.
|
||||
|
||||
The server owns the continuation envelope. It includes the triggering request, subsequent user direction, interaction outcomes, completed work, and an explicit history cursor. The latest request supplies the completion objective; an old task title cannot satisfy a new follow-up.
|
||||
|
||||
Confirmed provider descendant IDs survive restart and stay exact within a 4,096-entry inventory, with IDs bounded to 240 bytes. Capacity exhaustion stops provider work with an explicit reconciliation reason; it never evicts identities or pretends that a valid child is an integrity violation. Repeated progress diagnostics remain bounded.
|
||||
|
||||
Local CLI run-authored comments retain their provenance in history and cannot replace the latest human objective. Scheduled replacements and the final dispatch gate reject another run's execution or checkout lock.
|
||||
|
||||
A shared execution projection distinguishes confirmed work, recovery, scheduled retries, finalization, and real interaction waits. The composer remains usable. The failed predecessor remains inspectable after a replacement.
|
||||
|
||||
## Reproduced failure
|
||||
|
||||
A Codex notification named another thread. Recovery attempted an unusable checkpoint. On a later attempt, the admission transaction held the task row while waiting for provider spawn or adapter settlement. Failure finalization waited for the same row. PostgreSQL confirmed the blocking transaction. The final gate now initiates the adapter handoff while ownership is locked, then commits without awaiting provider work. Bootstrap and finalization can acquire the same rows independently; a competing owner cannot enter between the final check and handoff.
|
||||
|
||||
A connection continuation also omitted the follow-up that requested a second service. Its old completion objective referred to the first service. The continuation envelope now preserves the source request even when the preceding run already received that message.
|
||||
|
||||
The shared protocol-integrity and cleanup changes incorporate the relevant prerequisites from PR #13038. They do not require its chat feature.
|
||||
|
||||
## Functional evidence before the quiet UI revision
|
||||
|
||||
Five independent fresh source-CLI `test-drive` instances passed these browser journeys:
|
||||
|
||||
| Journey | Provider | Result |
|
||||
| --- | --- | --- |
|
||||
| Safe replacement | Native Codex driver with deterministic model and MCP fixtures | A second-service request survives the injected failure and receives a tool-backed answer without another Run click. |
|
||||
| Unknown action | Native Codex driver with deterministic fixtures | No speculative replay. The operator records action outcomes before continuation. |
|
||||
| Restart during retry | Native Codex driver with deterministic fixtures | Durable retry survives server restart with one successor. |
|
||||
| CEO descendant events | Native Codex driver with deterministic fixtures | Provider-confirmed descendant notifications do not crash or complete the root. |
|
||||
| Unsupported legacy recovery | Deterministic process adapter | Unknown action outcomes create an operator-owned recovery action. |
|
||||
|
||||
These fixtures do not prove live provider authentication. A separate retained live instance completed the current Gmail request with native Codex and model `gpt-5.6-sol`: one search call and five thread reads. History, assignment, and existing connections were retained. No mail was sent. Private provider history, instance identifiers, and credentials are excluded from this repository.
|
||||
|
||||
The live journey required explicit operator reconciliation during diagnosis. It proves the repaired functional path, not a frictionless first attempt.
|
||||
|
||||
## Automated checks before the quiet UI revision
|
||||
|
||||
Before PR rebase, the repository test groups, typecheck, build, token gates, and Storybook build passed. The server test groups ran in shards, with affected suites rerun after repairs. Runner TypeScript passed 1,665 tests with eight skips; Node contracts passed 38 tests; the full Rust workspace passed. After the rebase, the PR checks verify the new head, including the session-goal changes on master.
|
||||
|
||||
Focused coverage includes provider event identity, structured failure propagation, atomic finalization, cleanup failure, lease loss, publication recovery, one-successor dispatch, shared retry budgets, quota monitors, current reviewer authorization, ownership changes, continuation context, and uncertain actions. Legacy adapters need positive pre-provider evidence to authorize a bootstrap retry. A pre-provider workspace wait does not consume the failure budget.
|
||||
|
||||
## Browser and Storybook reproduction
|
||||
|
||||
Finish the runner build before browser acceptance. Do not rebuild generated provider artifacts while a fixture consumes them.
|
||||
|
||||
```sh
|
||||
pnpm exec playwright test --config tests/e2e/execution-recovery/playwright.config.ts recovery.spec.ts
|
||||
pnpm --filter @paperclipai/ui build-storybook
|
||||
RECOVERY_STORYBOOK_URL=http://127.0.0.1:6108 pnpm exec playwright test --config tests/e2e/execution-recovery/playwright.config.ts storybook.spec.ts
|
||||
```
|
||||
|
||||
Serve the built Storybook at the configured URL before the second browser command. The recovery suite creates and stops fresh test-drive instances itself. Each journey records its actual URL, data directory, checkout, task/run identifiers, provider calls, and screenshots in the Playwright output directory.
|
||||
|
||||
The quiet UI revision removes the execution status card, list badges, and reconciliation dialog. Existing transcript headers may briefly show Reconnecting. The source task and composer remain visible. Unknown action outcomes receive a durable automatic no-replay disposition; no operator questionnaire is shown.
|
||||
|
||||
Independently addressable stories under `tasks-execution-recovery`:
|
||||
|
||||
`working`, `reconnecting`, `retry-scheduled`, `waiting-for-workspace`, `finalizing`, `safely-replaced`, `recovery-exhausted`, `uncertain-action`, `unavailable-recovery`, `waiting-for-access`, `waiting-for-answer`, `narrow-long-error`, `composer-during-recovery`, `task-list-badges`, `task-list-badges-canonical`, `native-chat-status-labels`, `legacy-chat-status-labels`, `dashboard-status-labels`.
|
||||
|
||||
Open a story with `?path=/story/tasks-execution-recovery--<suffix>`. The badge story names remain stable for review links; their rows now demonstrate the absence of execution badges. Earlier screenshots of the status card and dialog are obsolete and are not acceptance evidence for this revision.
|
||||
|
||||
Representative screenshots for the quiet presentation:
|
||||
|
||||
- [Task lists without execution badges](../assets/execution-recovery/quiet-task-list.png)
|
||||
- [Temporary reconnection in the existing transcript header](../assets/execution-recovery/quiet-retry.png)
|
||||
- [Fixture continuation after refresh](../assets/execution-recovery/fixture-completed.png)
|
||||
|
||||
## Quiet recovery revision verification
|
||||
|
||||
All five fresh deterministic journeys passed. The uncertain-action and legacy journeys were then rerun with assertions for the visible blocked status, absence of the reconciliation form and Retry control, and preserved draft text. Both passed. A missing execution projection in the history response was fixed so the feed does not offer a retry that the server will reject.
|
||||
|
||||
The 72 Storybook theme/viewport combinations passed, with 16 affected combinations rerun after presentation changes. Repository typecheck, build, token gates, and Storybook build passed. Focused recovery, projection, activity-history, and UI tests passed. The full local suite exposed a fixture race that closed a task before provider acceptance; the fixture now waits for actual provider acceptance, and all 64 tests across the affected continuation and recovery-route suites pass.
|
||||
|
||||
Recovery decisions remain visible in local run logs. The durable status broadcast contains only run/agent identifiers, status, timestamps, and delivery ID; it does not broadcast provider output or errors. New verified evidence can clear an automatically settled hold through the existing authorized evidence API. Generic retries and duplicate requests cannot clear the hold. No dialog is exposed for this path.
|
||||
|
||||
## Rollout and limits
|
||||
|
||||
The recovery migrations were renumbered to 0250–0254 after master added session goals and action-delivery storage. Their SQL remains idempotent for instances that applied the earlier branch numbers. Migration snapshots include both sets of schema changes.
|
||||
|
||||
Control transitions have a 60-second deadline and a 15-second reconciliation cadence. With a healthy database and scheduler, abandoned transitions must be repaired or surfaced within 90 seconds. Healthy provider silence has no new timeout. An upgrade never automatically replays ambiguous historical work. Default CEO instructions remain unchanged.
|
||||
|
||||
Fresh test-drive instances use local-trusted mode. Authenticated/cloud browser behavior, every legacy provider, and every deployment topology were not exercised live. Automated tests cover authorization and company boundaries. An early terminal-delivery screenshot still showed Working briefly; the final refreshed screenshot above shows the completed state.
|
||||
|
||||
The final ownership-handoff regressions passed (42 tests across the dispatch adapter and stale-queue suites). They verify competing ownership at the handoff boundary and provider failure before a spawn callback, without retaining database locks for provider completion.
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# Experimental chat channel landing plan
|
||||
|
||||
The chat integration lands in two dependent changes after the native runner
|
||||
prerequisites in #13092. The runner change is not one of these two chat changes.
|
||||
|
||||
## First change: provider and data foundation
|
||||
|
||||
Add the closed channel types, additive database schema, pinned provider adapter
|
||||
patches, packaged dependencies, and opt-in runtime and transport helpers. Include
|
||||
their real-parser, synthetic-transport, migration, and package qualification tests.
|
||||
Keep the existing server routes, application catalog, experimental settings,
|
||||
heartbeat dispatch, and Board entry points unchanged. This change does not start
|
||||
provider connections or expose partially implemented channel routes. The existing
|
||||
production GitHub tool connection keeps its current path.
|
||||
|
||||
Validate this change against the existing master consumers independently. Run
|
||||
repository types, tests, and build, the historical database upgrade checks, and
|
||||
the release patch-packaging checks. Exact-head CI and code review are required.
|
||||
|
||||
The foundation exports the connection-purpose type needed by the schema, but
|
||||
does not add the channel transport or application kind to existing consumers.
|
||||
Durable command-registration and Teams transfer stores remain in the second
|
||||
change alongside their service authorization. Tenant foreign keys bind task,
|
||||
agent, comment, delivery, and action references to their company. Nullable
|
||||
deletion uses an ID-only `SET NULL` action plus a tenant `NO ACTION` constraint,
|
||||
so deleting a parent cannot clear the required company ID. Action references to
|
||||
conversation and principal retain history instead of silently detaching it.
|
||||
These corrections use forward migrations; historical SQL remains unchanged.
|
||||
|
||||
The runtime registry retires the prior endpoint owner before exposing a
|
||||
replacement, and fences initialization against removal or shutdown. It does not
|
||||
initialize providers itself: the service caller must install current guarded
|
||||
callback context before starting a Gateway connection.
|
||||
|
||||
## Second change: gated service and Board integration
|
||||
|
||||
Add company-scoped durable admission, identity and reach authorization, leases,
|
||||
queues, publications, native controls, and the Board user journey. This includes
|
||||
the provider-specific registration and service wiring for Slack, Discord,
|
||||
Telegram, Teams, and GitHub. Keep activation behind the experimental channel
|
||||
setting. A capability is not permission to bypass current source or user checks.
|
||||
|
||||
Run joined service and recovery tests, all repository checks, and the deterministic
|
||||
browser suite on the composed head. Live provider observations supplement these
|
||||
checks; mocked transport and browser fixtures are not live-provider proof.
|
||||
|
||||
Merge the foundation first. Then update the integration onto current master and
|
||||
repeat exact-head verification and review. Each chat pull request must remain
|
||||
under 500 changed files. Preserve uncertain delivery outcomes and explicit
|
||||
unsupported cases rather than claiming complete provider qualification.
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# Codex integration acceptance — 2026-09-09
|
||||
|
||||
## Environment and scope
|
||||
|
||||
All live tests used the pinned Codex CLI 0.153.4 and gpt-5.6-sol.
|
||||
Each fixture used an isolated Codex home. A repository contained known launch
|
||||
notes, a configuration marker, a skill marker, and a harmless SessionStart hook.
|
||||
The hook only appended a line to a fixture file.
|
||||
|
||||
Streaming and feed-display changes remain separate pull requests. These tests
|
||||
used their combined implementation checkout. Each PR is also checked on the
|
||||
current master before merge.
|
||||
|
||||
## Native browser acceptance
|
||||
|
||||
A fresh test-drive instance started without tasks or prior runs. The test agent
|
||||
used paperclip_runner with Codex. Browser actions created a task in the fixture
|
||||
project, requested the notes, and sent two follow-up messages. The test server
|
||||
was restarted between the first answer and the follow-ups.
|
||||
|
||||
All three native runs succeeded. They returned the expected notes and markers,
|
||||
then answered a date-change question and recalled the original reference.
|
||||
Answers persisted after refresh. No provider notice appeared. The task composer
|
||||
remained usable.
|
||||
|
||||
The task policy opened new provider threads for completed-task follow-ups.
|
||||
This browser test does not prove same-thread resume. The next tests do.
|
||||
|
||||
## Same-thread local driver acceptance
|
||||
|
||||
The production TypeScript driver ran an initial repository read, a follow-up,
|
||||
a provider shutdown, persisted-session recovery, and a second follow-up.
|
||||
All answers used the same provider thread and retained the required context.
|
||||
|
||||
- No provider notices appeared.
|
||||
- Configuration and skill markers loaded.
|
||||
- The approved hook ran once at startup and once at cold resume.
|
||||
- Run usage was 40,445 + 13,624 + 10,538 = 64,607 tokens.
|
||||
- The sum equaled the final cumulative session usage. Historical usage was
|
||||
not charged again.
|
||||
- Resume usage produced only the bounded local diagnostic.
|
||||
|
||||
## Daytona driver acceptance
|
||||
|
||||
A disposable Daytona sandbox ran the production TypeScript driver bundle.
|
||||
The test installed Codex 0.153.4 because the image had an older version.
|
||||
Startup, warm follow-up, process shutdown, cold resume, and the second
|
||||
follow-up all succeeded on the same provider thread.
|
||||
|
||||
- Configuration and skill markers loaded.
|
||||
- The approved hook ran once at startup and once at cold resume.
|
||||
- No trust, history-deprecation, or settled-turn usage warning appeared.
|
||||
- Run usage was 23,006 + 11,611 + 8,120 = 42,737 tokens.
|
||||
- The sum equaled the final cumulative session usage.
|
||||
- A separate warning about missing system bubblewrap remained visible.
|
||||
Codex used its bundled copy. The change does not suppress that warning.
|
||||
|
||||
The sandbox was deleted after evidence collection. Remote Paperclip UI and
|
||||
remote Rust execution were not tested.
|
||||
|
||||
## Hook trust and experience limits
|
||||
|
||||
Codex reviews hook hashes separately from repository trust. The test queried
|
||||
hooks/list and approved only the harmless fixture hash through config/batchWrite.
|
||||
Product code does not bypass this policy or copy operator configuration.
|
||||
|
||||
The task feed worked correctly. Two separate issues remain outside this change:
|
||||
a dashboard preview could retain old running text, and test-drive restart could
|
||||
use a different database when its saved port did not match its actual port.
|
||||
The fixture configuration was corrected before sending further messages.
|
||||
Existing test data was preserved. Transitions were sampled rather than filmed.
|
||||
|
||||
## Automated verification before PR preparation
|
||||
|
||||
- Codex/native-transport TypeScript: 333 passed.
|
||||
- Adjacent OpenCode/ACPX driver, accounting, and recovery tests: 49 passed.
|
||||
- Same-run attach and usage baseline regression: 4 passed.
|
||||
- Rust library, serialized: 226 passed.
|
||||
- Rust Codex integration: 72 passed, 1 ignored; two new pagination tests passed.
|
||||
- Production native server integration passed after rebuilding its stale fake
|
||||
provider. The test now asks Cargo to check binary freshness.
|
||||
- Repository typecheck and build passed.
|
||||
- Repository test groups have passing coverage after environment retests.
|
||||
General-server coverage was 7,217 passed and 30 skipped. Route coverage was
|
||||
2,175 passed and 4 skipped across 144 files.
|
||||
|
||||
The initial monolithic test command did not pass cleanly. Parallel test loads
|
||||
caused timeouts; isolated reruns passed. CLI database tests reached the macOS
|
||||
shared-memory limit; they passed after unused disposable fixture servers were
|
||||
stopped. No production behavior or timeout setting changed for those failures.
|
||||
The default parallel Rust library run also had Claude fixture transport failures;
|
||||
the complete serialized rerun passed. PR checks record validation after replaying
|
||||
these changes onto current master.
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
# Correct Codex startup trust, history reads, and resume notices
|
||||
|
||||
Date: 2026-09-09. Approved scope: three Codex integration fixes. This supersedes
|
||||
this document's earlier exploratory recommendations. Feed display and
|
||||
full-answer streaming are separate preceding changes.
|
||||
|
||||
## Product rules
|
||||
|
||||
1. Trust the execution root that Paperclip selects at startup. Resolve it on
|
||||
the execution host, including a Git worktree's main repository trust key.
|
||||
Write only the isolated Codex configuration. Keep sandbox, tool, and secret
|
||||
controls authoritative. Later directory changes do not grant new trust.
|
||||
2. Codex retains model conversation context. Paperclip reads provider state to
|
||||
establish execution authority or recover specific evidence. Normal resume
|
||||
must not download historical message contents.
|
||||
3. A resume usage snapshot describes completed work. It can establish a
|
||||
cumulative baseline, but must not charge that work to a new run or produce
|
||||
a warning. Other stale notifications and invalid authoritative events keep
|
||||
their existing validation.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Resume usage
|
||||
|
||||
Handle the exact root-thread `thread/tokenUsage/updated` snapshot before the
|
||||
settled-turn warning branch. Keep its reported historical turn identity and a
|
||||
bounded local `codex_resume_usage_snapshot` diagnostic. The Rust normalizer
|
||||
must not emit a billable usage event for the snapshot. The TypeScript driver
|
||||
persists its cumulative baseline with the existing checkpoint and reports a
|
||||
monotonic run delta. Attachment begins a new delta at the last observed total;
|
||||
recovery of the same run preserves its baseline. Repeated snapshots are not
|
||||
additional receipts. Missing thread or turn identity does not gain authority.
|
||||
|
||||
### Supported state and history reads
|
||||
|
||||
Use `excludeTurns: true` for resume and `includeTurns: false` for thread state.
|
||||
Request turn metadata with `thread/turns/list` and `itemsView: notLoaded`.
|
||||
Only request `thread/items/list` content for a specific turn when reconciliation
|
||||
needs its final answer, tool result, or completion evidence. Follow cursors,
|
||||
keep stable order, deduplicate IDs, reject repeated cursors and incomplete
|
||||
responses. A missing API reports a compatibility/read error; there is no
|
||||
Codex full-history fallback.
|
||||
|
||||
Rust uses lightweight idle/active state, then paginated metadata when it needs
|
||||
an active turn identity. The controller's runner transport serves targeted
|
||||
recovery evidence from committed runner events; it rejects content reads
|
||||
outside the retained turn window. Existing non-Codex proxy behavior stays
|
||||
separate from Codex's protocol requirements.
|
||||
|
||||
### Startup trust
|
||||
|
||||
Before spawning Codex, canonicalize the selected root and add its trusted
|
||||
project entry to the isolated config. Preserve unrelated settings and use a
|
||||
private atomic replacement. Start the provider process in that same root,
|
||||
so startup cannot load the Paperclip server checkout by accident. Persist the
|
||||
startup directory in the existing optional session checkpoint for cold resume.
|
||||
Remote roots are resolved on the execution host.
|
||||
|
||||
Retain the server-selected permission profile on subsequent TypeScript turns,
|
||||
including Daytona's existing external sandbox profile. Do not change approval
|
||||
policy or bypass the external sandbox boundary.
|
||||
|
||||
Repository trust loads hook definitions, but Codex 0.153.4 separately reviews
|
||||
individual hook hashes. Preserve that policy. Acceptance explicitly approves
|
||||
only the harmless fixture hook through Codex's supported config API; product
|
||||
code does not bypass hook trust or invoke provider hooks itself.
|
||||
|
||||
## Verification and exclusions
|
||||
|
||||
Use Codex CLI 0.153.4, the pinned supported baseline. Test snapshot replay and
|
||||
cold recovery accounting, cross-thread isolation, full-history avoidance,
|
||||
metadata/item pagination, incomplete evidence, worktree/non-Git/canonical
|
||||
trust, malformed config, and unchanged sandbox profiles. Run focused Rust,
|
||||
TypeScript, server lifecycle/accounting tests, then repository typecheck,
|
||||
tests, and build.
|
||||
|
||||
Use fresh local test-drive data and a disposable Daytona sandbox with real
|
||||
Codex. Verify an initial repository read, two follow-ups, cold resume, config
|
||||
and skill markers, one hook execution per startup/resume, exact cumulative
|
||||
usage arithmetic, and absence of the three original warnings. Inspect the
|
||||
browser answer after refresh. Record local/native and remote/TypeScript
|
||||
proof separately, including any environment warnings or unverified behavior.
|
||||
|
||||
No new UI, task state, public API, database migration, retry policy, or session
|
||||
replacement workflow. Preserve the Gmail handoff patch and existing test data.
|
||||
|
||||
See [acceptance evidence](2026-09-09-codex-integration-acceptance.md).
|
||||
|
|
@ -0,0 +1,390 @@
|
|||
# Paperclip Chat Adapters Architecture
|
||||
|
||||
**Status:** decision-complete implementation plan
|
||||
**Date:** 2026-09-03
|
||||
**Paperclip base:** `origin/master` at `8430bd897f01dd4b91e0970efffb71b97e5a2685`
|
||||
**Earlier planning references:** `origin/master` was initially observed at `b872cd3d1b404bdaff70af493a2973ceb7e5d6ec`, then refreshed through `112ef5beecf518ce9e0cbbead3eac297c09fc775`, `b84964e5a2fa8b1e6498a1ccb471f6adba97d470`, `7b094724e65c04949706df638d497afb02c84b62`, and `d593463ab6394cd356bf27448ea28bad8cccf4ec`; the implementation branch is rebased onto the SHA above.
|
||||
**Research snapshots:** Vercel Chat SDK `51322dde8f4aafd8a7fc7a20cbfd7ae45cafaa5c` (`chat` 4.39.0); OpenTag `6a770d862349f8e996c23c145aef6d6275914a23`
|
||||
|
||||
## 1. Decision summary
|
||||
|
||||
Paperclip will let a company expose any of its agents through external chat systems without turning those systems into a second control plane. External chat is transport and presentation. Paperclip remains authoritative for agents, tasks, runs, permissions, approvals, budgets, artifacts, liveness, and audit history.
|
||||
|
||||
The first release ships Slack, Microsoft Teams, Discord, Telegram, and GitHub. The architecture is registry- and capability-driven so Google Chat, Linear, Notion, WhatsApp, Twilio, X, Messenger, Instagram, email through Resend, iMessage providers, and vetted community adapters can be enabled without redesigning persistence or routing.
|
||||
|
||||
The decisive identity choice is **one native bot identity per Paperclip agent endpoint**. A Slack workspace may contain several Paperclip agents, but each is installed as a distinct Slack app/bot and addressed through its native mention. Paperclip will not hide several agents behind one dispatcher bot in v1.
|
||||
|
||||
Thread-capable providers use a Hermes-style activation model. A person mentions the bot in the channel's root timeline; the bot creates or opens a native thread rooted at that message, creates exactly one Paperclip issue for its endpoint, and moves the conversation into that thread. Slack and Discord can continue eligible replies in the bound thread without another mention. Teams uses the same post/reply boundary and its required app manifest requests the resource-specific consent needed to deliver unmentioned channel-thread replies; setup cannot complete until the live root-and-reply test proves that grant is effective. GitHub binds an existing issue, pull-request conversation, or inline review-comment thread rather than manufacturing a second GitHub thread.
|
||||
|
||||
## 2. Product invariants
|
||||
|
||||
1. A channel endpoint belongs to one company, one Apps connection, one adapter, and exactly one Paperclip agent. The endpoint's assigned agent is immutable after creation. Connecting a different agent requires a new connection/endpoint; the setup and connector-detail UI never offers **Change agent**.
|
||||
2. On a provider with `create_thread_from_root`, a new root-level native mention is an activation envelope: verify it, create or open the provider thread, create the Paperclip issue, reply in the thread, and leave the root timeline quiet except for the provider's normal thread indicator.
|
||||
3. One bot-owned external thread maps to exactly one Paperclip issue for that endpoint. The binding is idempotent by endpoint plus activation root/thread id. The exceptional case where another Paperclip bot joins through an explicit route still creates a separate related issue because Paperclip preserves single assignment; it may not silently share or steal the first endpoint's issue.
|
||||
4. Once bound, every eligible human message delivered from that provider thread continues the same Paperclip issue. Slack, Discord, and a correctly installed Teams app need no repeated mention. Paperclip requires and live-verifies Teams RSC rather than exposing a weaker mention-per-turn mode. Telegram privacy-on groups require a reply to the bot or another mention. A fresh unmentioned root message is ignored. A mention inside a pre-existing provider thread may activate that thread when policy allows, but it still yields only one issue binding.
|
||||
5. Providers without creatable native threads use a declared fallback: use the existing provider conversation/comment thread, or combine the stable conversation with an explicit Paperclip session generation. GitHub uses the existing issue/PR or inline review-comment thread. Telegram forum topics use `message_thread_id`; ordinary Telegram DMs/groups maintain one active issue until `/new`, **New task**, or `/close` advances/closes the binding.
|
||||
6. A provider's stable direct-message conversation key plus active session generation is the DM issue boundary. The first message creates the active issue; subsequent messages continue it; an explicit new-task action starts a new generation when the provider does not supply multiple native DM threads.
|
||||
7. The issue remains a normal Paperclip task. Its title, description, status, project, goal, priority, documents, and artifacts remain editable. Its assigned agent is locked to the endpoint agent for the lifetime of the external task. Connecting a different agent requires a new connection and a new external task; there is no normal detach-and-reassign flow.
|
||||
8. Incoming messages are attributed to an external principal. Linked principals act as their mapped Paperclip user. When enabled in Access, unlinked principals act only through the fixed restricted external profile; the internal sponsoring principal is not a selectable end-user role.
|
||||
9. Agent execution always uses the assigned Paperclip agent's existing adapter, runtime, permissions, budgets, checkout rules, and approval gates. A channel message never creates a new execution authority.
|
||||
10. Only a safe, explicitly external publication projection leaves Paperclip. Raw chain-of-thought, internal comments, tool traces, run logs, secrets, hidden activities, and internal identifiers do not.
|
||||
11. Agent output is eligible for automatic publication. Board comments are Paperclip-only unless their author explicitly chooses **Send to channel**.
|
||||
12. External agent-to-agent turns are disabled by default. Enabling them requires a directed route, endpoint allowlists, a bounded hop count, self-message suppression, causal fingerprints, and immutable audit events.
|
||||
13. Bring-your-own provider credentials is sufficient to ship. A managed Add to Slack path can be added later but cannot block the first release.
|
||||
14. Every active endpoint uses the maximum safe capability set available to its adapter, provider installation, current conversation type, and current Paperclip permission check. Reactions, streaming, rich messages/cards, buttons, modals, commands, files, edits, DMs, and private-response fallbacks are implementation behavior, not per-endpoint on/off settings. Capability negotiation selects the best legal path and degrades unsupported behavior to safe text plus a Paperclip URL; it never bypasses Paperclip authorization.
|
||||
|
||||
## 3. Ownership boundary
|
||||
|
||||
### 3.1 Native Paperclip chat-adapters subsystem
|
||||
|
||||
Chat adapters are part of Paperclip itself, not a bundled or separately installed plugin. The subsystem owns:
|
||||
|
||||
- company boundary and actor extraction;
|
||||
- external-principal authorization contract;
|
||||
- task creation, single assignment, checkout, wakeup, liveness, and budget gates;
|
||||
- externally bound task assignment lock and immutable binding lifecycle;
|
||||
- safe-publication projection and secret/redaction policy;
|
||||
- attachment ingestion and work-product creation;
|
||||
- activity records for every mutation;
|
||||
- public ingress registration and raw-body access needed for signature verification;
|
||||
- secret references and credential resolution;
|
||||
- adapter registry and endpoint lifecycle;
|
||||
- Chat SDK adapter construction;
|
||||
- provider webhook verification and normalized event conversion;
|
||||
- provider-thread creation and reconciliation;
|
||||
- activation/subscription rules;
|
||||
- conversation and task binding;
|
||||
- delivery, action, and publication workers;
|
||||
- provider rendering, streaming, reactions, cards, modals, and fallbacks;
|
||||
- relay client/server protocol;
|
||||
- Apps, agent, task, identity-link, and diagnostics UI surfaces;
|
||||
- first-party schema migrations and lifecycle controls.
|
||||
|
||||
### 3.2 Chat SDK
|
||||
|
||||
Use Chat SDK for platform-specific normalization and presentation, not as Paperclip's authority. Chat SDK supplies:
|
||||
|
||||
- provider adapters and signature helpers;
|
||||
- mentions, subscribed messages, reactions, slash commands, actions, and modals;
|
||||
- message/card/file abstractions;
|
||||
- native streaming where available and post-plus-edit fallbacks elsewhere;
|
||||
- direct messages and ephemeral-message fallbacks;
|
||||
- provider capability differences.
|
||||
|
||||
Paperclip supplies a database-backed Chat SDK state adapter. In-memory and standalone Redis state may be used in adapter unit tests, but never as the production source of truth for endpoint subscriptions, locks, queues, history, or task bindings.
|
||||
|
||||
## 4. Apps model and connection identity
|
||||
|
||||
Apps remains the only integration catalog and `/apps` remains the only discovery and setup entry point. Add a `chat_sdk` transport and a `channel` purpose to the connection contract. A provider may expose two separate methods:
|
||||
|
||||
- **Chat with an agent** — a channel connection accepting inbound conversation and publishing task output.
|
||||
- **Use this connection as an agent tool** — the existing tool-connection path, granting provider actions to agents under the existing credential and human-access model.
|
||||
|
||||
These methods may share provider branding but never silently share credentials, grants, or identity. The UI must always name which direction is being configured.
|
||||
|
||||
The connection-purpose choice is conditional, not a permanent extra wizard step. Show it for every selected provider whose registry entry exposes both chat and tool connection surfaces, not through a provider-name exception. A chat-only provider skips directly to **Which agent do you want to chat with?** using Paperclip's existing single-agent selector. Selection is final for that endpoint.
|
||||
|
||||
Provider setup then uses a persistent step-rail wizard with one focused external handoff per phase. The completed agent-selection step remains visible in the rail, but the page body never repeats the selected agent. The wizard preserves completed steps across provider redirects/admin waits and gives every button an explicit consequence. It does not repeat reach, behavior, route, capability, transport, automatic work, or successful verification results. A setup screen may contain only something the operator must click, choose, copy, paste, upload, run, or perform at the provider during that phase. Errors and missing prerequisites appear only when they occur. A successful real provider message completes the connection; **Save & exit** preserves an unfinished draft.
|
||||
|
||||
Default installation must minimize exposed credentials while keeping the bring-your-own path complete:
|
||||
|
||||
- customer-owned Slack Apps request only Bot User OAuth Token and Signing Secret and treat them as write-only;
|
||||
- Paperclip generates and stores GitHub's webhook secret, reveals it once for copying to GitHub, and requests only the App ID and private-key PEM;
|
||||
- Teams requests Client ID, tenant ID, and client secret from the customer-owned Entra App/Azure Bot registration;
|
||||
- Telegram requests the BotFather bot token because BotFather has no OAuth installation callback.
|
||||
|
||||
The customer-owned Slack App path opens a prepared Slack App Manifest, instructs the operator to create and install it, then requests only Bot User OAuth Token and Signing Secret before the channel mention/thread-reply test. A standardized **Add to Slack** handoff may be added later when Paperclip has access to that program; it is optional, may not change runtime authority, and cannot gate the BYO path or release.
|
||||
|
||||
All nonessential configuration is post-connect. A chat connection reuses the current connector-detail shell with provider-specific `Settings`, `Access`, `Conversations`, and `Activity` tabs. There is no read-only Overview tab. Settings contain only destination reach that an operator can plausibly change: allowed channels, repositories, chats, or topics, plus direct-message and group-chat toggles where the provider supports those surfaces. The assigned agent, provider account/workspace, task boundaries, activation rules, delivery transport, credentials, installation drift, and response capabilities are not settings.
|
||||
|
||||
Provider installation is an availability ceiling, not Paperclip authorization. Slack/Discord/Teams channel membership, Telegram chat membership, and the repositories selected in a GitHub App installation determine the resources whose events the provider can deliver. Paperclip independently enables a subset of those resources. Effective reach is the intersection of provider availability, Paperclip enablement, active endpoint state, and current actor authorization.
|
||||
|
||||
The destination used to complete the setup test becomes the connection's first enabled resource because the operator explicitly selected and exercised it. A channel, chat, topic, or repository discovered later appears in Settings as available but disabled. Invitation or installation alone never creates a task or permits a response. Enabling a resource that is not currently available at the provider is rejected with the appropriate provider action, such as **Add Maya to Slack** or **Manage GitHub installation**. Losing provider membership or repository access marks the resource unavailable, blocks new work, and preserves existing task and conversation history.
|
||||
|
||||
The management tabs therefore have deliberately separate jobs:
|
||||
|
||||
- **Settings** controls where the connection may act inside the provider's available resource set.
|
||||
- **Access** controls who external people represent. Linked identities use current Paperclip user permissions; allowed unlinked identities use the fixed restricted external profile. The endpoint's sponsoring principal remains an internal authority ceiling and audit field, not ordinary UI configuration.
|
||||
- **Conversations** is a read-only cross-link list: external conversation, Paperclip task, current state, **Open provider**, and **Open task**. It has no binding controls, detach action, or boundary explainer.
|
||||
- **Activity** contains delivery health, redacted errors, replay, and contextual repair actions.
|
||||
|
||||
The first release makes these product choices instead of exposing policy selectors:
|
||||
|
||||
- a root mention in Slack or Teams creates a provider-native thread and one Paperclip task; later replies in that bound thread continue the same task without another mention when the provider delivers them;
|
||||
- the first mention inside an unbound existing Slack or Teams thread binds that thread to one new task from that point forward and does not import earlier history;
|
||||
- a DM has one open task at a time; after that task completes, the next message creates a new task, while **New task** or `/new` starts another explicitly;
|
||||
- a GitHub mention binds the addressed issue, pull-request conversation, or inline review thread to one task;
|
||||
- Telegram DMs and ordinary groups use one open task at a time, while a forum topic has one stable topic-to-task binding;
|
||||
- direct verified webhook versus outbound relay is selected by instance deployment and reachability, not by the endpoint operator;
|
||||
- credential replacement, revoked installations, missing membership, and permission drift appear only as contextual reconnect/repair actions in Activity;
|
||||
- linked users use current Paperclip permissions, allowed unlinked users receive the fixed restricted external profile, overlapping turns queue, only safe milestones and final output publish, and agent-to-agent routes remain off.
|
||||
|
||||
Paperclip always uses the maximum safe provider capability set. Activity owns health, delivery diagnostics, and conditional repair actions. Relay and provider-specific developer transports live under instance administration, not endpoint onboarding.
|
||||
|
||||
Each live channel connection has one `chat_endpoints` row. Creating a second bot for another agent creates another connection/endpoint, even inside the same provider workspace. Bot display name and avatar default from the agent, while provider-specific immutable identity fields are displayed separately.
|
||||
|
||||
## 5. Persistence model
|
||||
|
||||
All records carry `company_id`, timestamps, and appropriate foreign keys. These are first-party Paperclip tables in the normal database schema and migration lifecycle.
|
||||
|
||||
### `chat_endpoints`
|
||||
|
||||
One-to-one with the parent Apps connection. Fields include adapter slug/version, immutable assigned agent, public endpoint id, provider account/workspace identity, bot identity, internal sponsoring principal, deployment mode (`direct | relay`), lifecycle status (`draft | verifying | active | paused | attention | revoked | archived`), and versioned behavior policy. Credentials are secret references on the parent connection, never inline JSON. The deployment mode is selected by instance reachability/policy and reported to the endpoint; it is not a connector-wizard preference. The sponsoring principal is derived from the connection owner or an instance policy and is not exposed as a normal endpoint setting.
|
||||
|
||||
Unique: parent connection; public endpoint id; provider bot identity within an installation where the provider requires it.
|
||||
|
||||
### `chat_endpoint_resources`
|
||||
|
||||
Provider-available external resources such as Slack channels, Teams conversations, Discord servers/channels, Telegram groups/topics, GitHub repositories, Notion pages, phone numbers, or email domains. Store normalized resource type/id, human label, provider availability (`available | unavailable | removed`), Paperclip enablement, discovery source/time, last verification time, and provider-specific membership/install metadata. Only an available and enabled resource may activate or continue work.
|
||||
|
||||
Unique: endpoint plus provider resource type/id.
|
||||
|
||||
### `chat_external_principals`
|
||||
|
||||
Normalized external users and bots. Store provider tenant/workspace id, provider principal id, principal kind, display metadata, last-seen time, and disabled/deleted markers. Never treat display names or email addresses as identity keys.
|
||||
|
||||
Unique: company, adapter, provider tenant, provider principal id.
|
||||
|
||||
### `chat_identity_links`
|
||||
|
||||
Explicit mapping from an external principal to one Paperclip user, with creator, confirmation time, revocation, and last authorization check. Links are company-scoped and never inferred from matching email alone.
|
||||
|
||||
Unique: company plus external principal. A principal has at most one active Paperclip-user mapping in a company.
|
||||
|
||||
### `chat_conversations`
|
||||
|
||||
Maps endpoint plus normalized external conversation/thread identity to one Paperclip issue. Store conversation kind, thread activation mode, activation root message/event id, provider thread id, provisioning state, subscription state, activation source, issue id, lifecycle (`active | completed | unavailable | endpoint_removed`), latest inbound/outbound ids, and timestamps. A provisional row keyed by the root activation survives a crash between Paperclip issue creation and provider-thread creation and lets reconciliation finish without duplicating either side. Lifecycle changes never unlock agent reassignment or erase the historical link.
|
||||
|
||||
Unique: endpoint plus activation root message id; endpoint plus external conversation/thread id. An issue has at most one active binding for the same endpoint.
|
||||
|
||||
### `chat_deliveries`
|
||||
|
||||
Durable inbound ledger. Store provider event id, normalized kind, raw payload digest, a bounded/redacted normalized envelope, receipt time, processing state (`received | processing | applied | ignored | retrying | failed | dead_letter`), attempt count, lease, result references, and redacted error. The raw provider payload is retained only when explicitly enabled with bounded TTL and encryption.
|
||||
|
||||
Unique: endpoint plus provider event id; otherwise endpoint plus deterministic payload fingerprint for providers without stable event ids.
|
||||
|
||||
### `chat_message_links`
|
||||
|
||||
Maps an inbound comment/action or outbound publication to provider message ids. Store direction, message/thread ids, revision, deletion state, and the Paperclip comment/publication/action reference.
|
||||
|
||||
### `chat_publications`
|
||||
|
||||
Durable outbound outbox. Store source kind/id, safe payload version, idempotency key, target conversation, rendering plan, lifecycle (`queued | streaming | posted | edited | delivered | retrying | failed | suppressed`), attempt data, and provider result.
|
||||
|
||||
Unique: endpoint plus idempotency key.
|
||||
|
||||
### `chat_actions`
|
||||
|
||||
Stores action/button/select/modal/slash-command callbacks with action id, principal, target interaction or command, payload digest, permission result, exact-once result, and provider acknowledgement.
|
||||
|
||||
Unique: endpoint plus provider action id or callback fingerprint.
|
||||
|
||||
### `chat_agent_routes`
|
||||
|
||||
Directed source-endpoint to destination-endpoint rules. Store activation mode, permitted external resources, maximum hop count, enabled state, and creator. Reject self-routes and cross-company routes.
|
||||
|
||||
### `chat_endpoint_leases`
|
||||
|
||||
Short durable leases for delivery processing, per-conversation sequencing, publication streaming, and relay ownership. A lease has resource kind/key, owner, fencing token, heartbeat, and expiry.
|
||||
|
||||
Unique: endpoint plus resource kind/key.
|
||||
|
||||
### `chat_sdk_state`
|
||||
|
||||
Versioned endpoint-scoped key/value records for Chat SDK state that cannot safely be derived. Known categories are subscriptions, provider cursors, adapter history, and SDK locks. Keys are bounded and values are schema/version checked.
|
||||
|
||||
## 6. Shared contracts and APIs
|
||||
|
||||
### 6.1 Shared types
|
||||
|
||||
Add stable shared types for:
|
||||
|
||||
- `ChatAdapterSlug`, `ChatAdapterMaturity`, and `ChatAdapterCapabilities`;
|
||||
- `ChatEndpoint`, `ChatEndpointStatus`, and redacted endpoint summaries;
|
||||
- normalized event kinds: root mention, thread message, subscribed message, DM, reaction, file, edit, delete, action, modal, slash command, lifecycle;
|
||||
- `ChatActivationPolicy`, `ChatThreadPolicy`, `ChatDmPolicy`, `ChatConcurrencyPolicy`, `ChatProgressPolicy`, `ChatFailurePolicy`, and `ChatPublicationPolicy`;
|
||||
- thread capabilities and modes: `create_thread_from_root | use_existing_thread | conversation_is_thread`, plus provider thread provisioning/reconciliation state;
|
||||
- `ExternalPrincipalRef` and external actor attribution;
|
||||
- safe publication text, artifact, card, interaction, and link blocks;
|
||||
- delivery/publication state and redacted diagnostics;
|
||||
- agent-route source stamps and hop metadata;
|
||||
- adapter setup fields derived from a pinned Chat SDK catalog snapshot.
|
||||
|
||||
### 6.2 Company and endpoint APIs
|
||||
|
||||
All authenticated APIs are under `/api`, company-scoped, and use existing HTTP/error conventions.
|
||||
|
||||
```text
|
||||
GET|POST /companies/:companyId/chat/endpoints
|
||||
GET|PATCH|DELETE /chat/endpoints/:endpointId
|
||||
POST /chat/endpoints/:endpointId/test
|
||||
POST /chat/endpoints/:endpointId/pause
|
||||
POST /chat/endpoints/:endpointId/resume
|
||||
POST /chat/endpoints/:endpointId/reconnect
|
||||
GET|PUT /chat/endpoints/:endpointId/resources
|
||||
GET|PUT /chat/endpoints/:endpointId/behavior
|
||||
GET /chat/endpoints/:endpointId/principals
|
||||
POST /chat/endpoints/:endpointId/principals/:principalId/link-intent
|
||||
DELETE /chat/endpoints/:endpointId/principals/:principalId/link
|
||||
GET|PUT /chat/endpoints/:endpointId/routes
|
||||
GET /chat/endpoints/:endpointId/conversations
|
||||
GET /chat/endpoints/:endpointId/deliveries
|
||||
POST /chat/endpoints/:endpointId/deliveries/:deliveryId/replay
|
||||
GET /chat/endpoints/:endpointId/publications
|
||||
GET|POST /chat/endpoints/:endpointId/relay
|
||||
POST /chat/endpoints/:endpointId/relay/rotate-key
|
||||
DELETE /chat/endpoints/:endpointId/relay
|
||||
```
|
||||
|
||||
### 6.3 Public ingress and linking
|
||||
|
||||
```text
|
||||
POST /api/chat/webhooks/:publicEndpointId
|
||||
GET /chat/link/:oneTimeToken
|
||||
POST /api/chat/link/:oneTimeToken/confirm
|
||||
```
|
||||
|
||||
The endpoint id is random and unguessable but is not treated as the authentication secret. Each adapter verifies the provider signature/token against the exact raw request body before a delivery becomes processable. Verification challenges are handled without starting a task.
|
||||
|
||||
One-time identity links are short-lived, single-use, bound to company/endpoint/principal, and completed only after Paperclip authentication. The confirmation page displays both identities and the target company before mutation.
|
||||
|
||||
### 6.4 Task APIs
|
||||
|
||||
Task responses include a derived, redacted `externalChannelBinding` summary. Add operations to inspect the immutable binding and explicitly publish a board-authored comment or existing eligible output.
|
||||
|
||||
```text
|
||||
GET /issues/:issueId/chat-binding
|
||||
POST /issues/:issueId/chat-publications
|
||||
```
|
||||
|
||||
Attempting to change `assigneeAgentId` on an externally connected task returns `409 chat_binding_agent_locked` with a safe explanation that a different agent requires a new connection. Removing an endpoint or losing provider access changes the binding lifecycle to unavailable but does not unlock reassignment or erase attribution, messages, publications, or activity.
|
||||
|
||||
## 7. Durable event flows
|
||||
|
||||
### 7.1 Inbound message
|
||||
|
||||
1. Resolve the public endpoint and read the raw request under strict size/time limits.
|
||||
2. Verify the provider signature/token before parsing untrusted fields for routing.
|
||||
3. Insert the delivery ledger row and return the provider's acknowledgement within its deadline. Slow work continues from the durable row.
|
||||
4. Claim the delivery with a fencing lease; duplicate claims return the existing result.
|
||||
5. Normalize event, tenant, resource, conversation, thread, sender, attachments, and causal ids through Chat SDK.
|
||||
6. Resolve or create the external principal. Suppress self messages and known outbound echoes.
|
||||
7. Enforce endpoint status, provider availability, Paperclip resource enablement, rate limits, route policy, and principal authorization. A valid event from an available but disabled resource is recorded as ignored with only the minimum safe metadata; it creates no task, wakes no agent, and sends no response.
|
||||
8. Apply the adapter's thread policy. For a root mention on Slack, Discord, or a thread-capable Teams channel, claim an activation lease keyed by endpoint plus root event/message id. For GitHub, claim the existing issue/PR/discussion thread. Unaddressed fresh root messages are recorded as ignored.
|
||||
9. Transactionally create the assigned Paperclip issue and a provisional conversation row before any non-idempotent provider call. The issue includes source metadata and a backlink, but no provider secret.
|
||||
10. Create/open the native provider thread through Chat SDK and finalize the binding. On Slack, the first bot reply under the root message establishes the thread; on Discord, create a native thread; on Teams, reply within the stable channel-post thread when supported. A crash leaves a reconcilable provisional binding rather than a second issue.
|
||||
11. For an already bound provider thread, append every eligible human reply to the same issue without requiring another mention. A mention inside an unbound pre-existing thread may bind it once when endpoint policy allows.
|
||||
12. Persist the incoming message/attachment as an issue comment or typed interaction with immutable external attribution.
|
||||
13. Use Paperclip's normal wakeup path. Existing checkout, active-run, budget, pause, and liveness rules decide whether work queues, steers, or waits.
|
||||
14. Publish the acknowledgement and all later output inside the bound provider thread. Use a reaction only as an optional immediate receipt; otherwise use ephemeral or concise threaded output. Record every mutation and final delivery state.
|
||||
|
||||
### 7.2 Thread activation modes
|
||||
|
||||
| Mode | Providers | Activation and binding |
|
||||
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `create_thread_from_root` | Slack, Discord, thread-capable Teams channels | A root `@bot` mention creates/opens a native thread and exactly one endpoint-owned Paperclip issue. All output stays in that thread; follow-ups continue there when delivered under the provider's mention/subscription/RSC rules. |
|
||||
| `use_existing_thread` | GitHub issues, pull-request conversations, inline review-comment threads; providers where the mention is already inside a native thread | The addressed existing thread becomes the external boundary and maps once to one endpoint-owned Paperclip issue. |
|
||||
| `conversation_is_thread` | Telegram chats/topics and providers without nested threads | A stable topic maps directly; a linear chat combines its stable key with an active session generation advanced by **New task**/`/new`. Activation copy makes the broader visibility explicit. |
|
||||
|
||||
Thread creation is capability-driven, never inferred from provider name alone. The registry records whether a surface can create a thread, whether the root message itself is the thread key, and whether bot replies, edits, files, actions, and streaming are legal inside it.
|
||||
|
||||
### 7.3 Outbound publication
|
||||
|
||||
1. An eligible agent result, interaction, or explicit board **Send to channel** action emits an outbox candidate.
|
||||
2. The safe-projection service validates visibility and produces a versioned payload containing only external text, approved links, sanitized artifacts, and supported interactions.
|
||||
3. The publication worker claims the per-conversation lease and renders against adapter capabilities.
|
||||
4. Prefer native streaming where supported. Otherwise post a working message and edit it at a bounded cadence. If editing is unsupported, post coarse milestones and one final response.
|
||||
5. Store provider ids after every acknowledged send. Retries use the same idempotency key and edit the known message where possible.
|
||||
6. On success, link the provider message to the Paperclip source. On terminal failure, retain a visible diagnostic and Paperclip retry control without mutating the task result.
|
||||
|
||||
### 7.4 Interactive callback
|
||||
|
||||
1. Verify and durably record the action exactly like other ingress.
|
||||
2. Resolve the external principal and its current Paperclip mapping.
|
||||
3. Re-read the target task/interaction and its current resolver audience or approval policy.
|
||||
4. Authorize as the linked Paperclip user. An allowed unlinked principal may answer only non-governed interactions allowed by the restricted profile; it cannot approve, hire, spend, change permissions, change budgets, or reassign agents.
|
||||
5. Apply the Paperclip mutation transactionally and exactly once. Resolution never implies authorization for its downstream effect.
|
||||
6. Return an ephemeral/card update where supported or a text result with a Paperclip URL.
|
||||
|
||||
## 8. Identity and permission model
|
||||
|
||||
### Linked principals
|
||||
|
||||
A linked principal becomes a Paperclip user actor only after explicit confirmation. Every action is reauthorized using current membership and permissions; a stale link conveys no cached authority. Activity includes provider/tenant/principal, Paperclip user, endpoint, delivery/action id, and authorization result.
|
||||
|
||||
### Restricted external principals
|
||||
|
||||
Every endpoint has an internal sponsoring principal and a versioned restricted external profile. The sponsoring principal is derived from the connection owner or instance policy; it is an audit and authority ceiling, not an Access-tab choice. Effective unlinked authority is the intersection of that ceiling, the enabled endpoint resource, restricted-profile operations, and target-state constraints. The initial allowlist is limited to starting/continuing the endpoint's bound task, uploading allowed attachments, and answering explicitly guest-resolvable non-governed questions. Unlinked people cannot use Paperclip as a general API principal and cannot govern the company. The Access tab exposes only whether unlinked participation is allowed and the explicit linked-identity list.
|
||||
|
||||
### Agent messages
|
||||
|
||||
Messages from another Paperclip bot resolve as external bot principals. They are ignored unless a matching directed route is active. Routed events carry an immutable origin endpoint, publication id, route id, visited endpoint set, and hop count. The receiving endpoint creates/continues its own task. Exceeding the hop limit, revisiting an endpoint, repeating a causal fingerprint, or targeting the source endpoint suppresses the event and writes audit evidence.
|
||||
|
||||
## 9. Chat SDK feature policy
|
||||
|
||||
The table below is an implementation contract, not a menu of endpoint toggles. For each publication or callback, Paperclip intersects adapter capabilities, provider installation/permission health, conversation type, safe-publication rules, and the current actor's Paperclip authority. It then uses the most capable legal rendering or interaction path automatically.
|
||||
|
||||
| Feature | Paperclip behavior |
|
||||
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Mentions and thread messages | A root mention creates/opens a provider thread and one issue when supported; later messages in that thread continue without mentions; fresh unaddressed root messages are silent. |
|
||||
| Streaming | Safe text only; native stream, draft preview, or post/edit fallback selected per adapter. |
|
||||
| Cards | Render safe artifacts, status, questions, approvals, and links; fall back to text plus Paperclip URL. |
|
||||
| Actions/dropdowns | Resolve typed Paperclip interactions after identity and permission checks. |
|
||||
| Modals | Use for provider-supported forms; validate again server-side and fall back to link. |
|
||||
| Slash commands | Map registered commands to explicit chat-subsystem operations where the adapter exposes command events. Slack `status`, `new`, and `close` controls are DM-scoped because Slack's slash-command payload has a channel id but no native thread timestamp; channel work remains managed from its mention-created thread and Paperclip task link. On Telegram, parse the small `/new`, `/status`, and `/close` vocabulary as ordinary messages. Never treat arbitrary command text as board authority. |
|
||||
| Emoji/reactions | Use a provider-safe acknowledgement vocabulary; custom emoji is optional. |
|
||||
| Files | Inbound files use bounded sanitized attachment ingestion; outbound files use signed, expiring content URLs or provider upload. |
|
||||
| Direct messages | One open task is active per DM conversation. After completion, the next inbound message creates a new task; **New task** or `/new` starts another explicitly. Proactive DM requires endpoint policy and target authorization. |
|
||||
| Ephemeral messages | Preferred for denials, link prompts, and private receipts; fall back to DM or safe public text. |
|
||||
| Overlap/concurrency | Support burst, queue, debounce, drop, and concurrent modes; default to queue and serialize task mutation. |
|
||||
| Edits/deletes | Map provider edits/deletes to append-only correction/tombstone events; never silently rewrite audit history. |
|
||||
|
||||
Safe progress states are `queued`, `working`, `waiting_for_input`, `approval_needed`, `completed`, and `failed`. They may name the current task phase or public artifact, but not private prompts, hidden tools, internal logs, or chain-of-thought.
|
||||
|
||||
## 10. Deployment model
|
||||
|
||||
### Direct mode
|
||||
|
||||
Paperclip exposes `/api/chat/webhooks/:publicEndpointId` at a stable HTTPS origin. This is selected automatically for cloud and publicly reachable authenticated/self-hosted instances. Provider credentials and signing material are secret references. Health checks confirm reachability, credential validity, subscription state, and a real test event. The endpoint wizard never asks the user to choose “direct webhook.”
|
||||
|
||||
### Relay mode
|
||||
|
||||
A private instance opens an outbound authenticated WebSocket to a lightweight relay. Providers send to the relay; the relay verifies its outer endpoint binding and forwards an encrypted, bounded envelope. Paperclip still performs provider signature verification before processing. The relay retains only retry metadata and encrypted payloads for a short configured TTL, has no Paperclip user credential, and cannot invoke arbitrary APIs. Fenced endpoint ownership prevents two connected instances from consuming one delivery. Instance administration selects/configures relay once; individual endpoint wizards inherit it automatically.
|
||||
|
||||
### Non-shipped managed installation
|
||||
|
||||
Bring-your-own provider credentials are the required first-release default. Managed Slack or GitHub provisioning may later reduce credential handling, but these are optional conveniences rather than separate runtime or permission models and cannot block activation or release.
|
||||
|
||||
Slack Socket Mode and Telegram polling are not ordinary endpoint choices. They are instance-level developer/on-premises escape hatches used only when the deployment cannot accept provider callbacks and has no configured relay. Enabling either requires explicit instance administration and provider-specific credentials; normal connector setup continues to say only that delivery is automatic.
|
||||
|
||||
## 11. Delivery phases
|
||||
|
||||
1. **Core contracts and mock adapter:** shared types, first-party schema and migrations, Paperclip state adapter, public ingress, delivery/outbox workers, safe projection, thread-provisioning state machine, and exhaustive mock-provider tests.
|
||||
2. **Apps and task surfaces:** extend the existing `/apps` setup shell with the registry-driven conditional purpose choice, immutable single-agent selector, persistent provider step rail, resumable external handoffs, and documented action consequences; add chat-specific connector-detail navigation, the agent Channels view, task binding banner, identity linking, and diagnostics. Delivery and capability mechanics never become onboarding questions.
|
||||
3. **Slack:** implement the complete guided customer-owned App path, signed callbacks/relay inheritance, root-mention-to-thread activation, one-thread/one-issue binding, threaded follow-ups, DMs, reactions, files, streaming/edit fallback, cards/actions/modals/commands, and a real-workspace harness. Add to Slack remains optional when platform access becomes available.
|
||||
4. **Teams, Discord, Telegram, and GitHub:** implement adapter-specific setup and capability tests against the same contracts. Teams and Discord exercise native thread behavior; Telegram exercises active linear-chat generations and forum-topic boundaries; GitHub exercises existing issue/PR/review-comment bindings. GitHub Discussions are deferred unless adapter support is added and tested.
|
||||
5. **Private relay:** outbound registration, rotation, reconnect, backlog limits, and failover diagnostics.
|
||||
6. **Agent routes:** directed allowlists, causal stamps, loop/hop protection, and multi-bot channel tests.
|
||||
7. **Catalog expansion:** enable official and reviewed vendor/community adapters by capability and maturity; no schema redesign.
|
||||
8. **Managed provisioning expansion:** broaden Slack organization deployment and other provider-managed installation paths without changing endpoint identity, task, permission, or transport contracts.
|
||||
|
||||
Each phase ships behind endpoint-level maturity flags (`experimental | preview | stable`). Migrations are additive. Pausing an endpoint or disabling chat adapters at the instance level stops new ingress/publications but preserves tasks, comments, attachments, and audit history.
|
||||
|
||||
## 12. Test and release gates
|
||||
|
||||
The live provider procedure, fixture identities, evidence contract, negative permission cases, cleanup, and per-platform browser steps are defined in [`2026-09-04-chat-adapters-browser-e2e-runbook.md`](./2026-09-04-chat-adapters-browser-e2e-runbook.md). That runbook is the stable-adapter acceptance gate; the lower-level tests below remain independently required.
|
||||
|
||||
- Company-boundary tests for every record, API, webhook lookup, replay, identity link, resource-enable action, and route.
|
||||
- Raw-body signature fixtures and replay/deduplication races for each adapter.
|
||||
- Transaction and crash-reconciliation tests for root mention, provider-thread creation, one-issue binding, existing-thread activation, immutable assignment locks, and endpoint/resource removal.
|
||||
- Linked-user, revoked-link, unlinked-disabled, restricted-external, sponsoring-principal-revoked, low-trust, governance-denied, and stale-target authorization tests.
|
||||
- Publication redaction tests proving secrets, raw traces, hidden comments, and internal-only artifacts never render.
|
||||
- Retry/idempotency tests for receipt-before-ack, worker crash, provider timeout, duplicate callback, stream resumption, and edit fallback.
|
||||
- Concurrency tests for all five overlap modes with Paperclip task mutation serialized correctly.
|
||||
- Attachment tests for size, type, checksum, malware/sanitization hooks, signed URLs, and provider expiration.
|
||||
- Agent-route tests for default deny, directed allow, self suppression, repeated causal fingerprint, hop bound, and two bots sharing one provider thread.
|
||||
- Direct and relay deployment tests, including relay disconnect/backlog/credential rotation and competing consumers.
|
||||
- UI tests for setup, provider-available versus Paperclip-enabled reach, permissions, identity linking, immutable task assignment, explicit publication, conversation cross-links, diagnostics, empty/error/revoked states, and responsive layouts.
|
||||
- Live smoke per stable adapter: root mention, provider thread creation/opening, exactly one issue, unmentioned threaded follow-up, silent fresh unmentioned root message, DM continuation, file, interaction, progress/final publication, duplicate event, and permission denial.
|
||||
|
||||
The first stable release is complete when an operator can connect any active Paperclip agent to Slack, Teams, Discord, Telegram, or GitHub; explicitly enable a subset of provider-available resources; an addressed native thread/object or explicit linear-chat session in that subset creates exactly one bound Paperclip issue for that endpoint; eligible follow-ups continue it using the provider's documented reply/mention rule; the existing Paperclip agent runs it under normal governance; safe output and artifacts return to the same conversation; failures are diagnosable and retryable; and every state transition is auditable. The provider-specific setup, permission, boundary, and fallback contract is maintained in `2026-09-04-chat-adapters-platform-surfaces.md`; the current navigation and UI inventory is maintained in `2026-09-04-chat-adapters-ui-surfaces-v8.md` and `index.html`.
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
# Chat Adapters Research Notes
|
||||
|
||||
**Date:** 2026-09-03
|
||||
**Paperclip implementation base:** `8430bd897f01dd4b91e0970efffb71b97e5a2685`
|
||||
**Earlier planning references:** `origin/master` was initially observed at `b872cd3d1b404bdaff70af493a2973ceb7e5d6ec`, then refreshed through `112ef5beecf518ce9e0cbbead3eac297c09fc775`, `b84964e5a2fa8b1e6498a1ccb471f6adba97d470`, `7b094724e65c04949706df638d497afb02c84b62`, and `d593463ab6394cd356bf27448ea28bad8cccf4ec`; the implementation branch is rebased onto the SHA above.
|
||||
**Vercel Chat SDK snapshot:** `51322dde8f4aafd8a7fc7a20cbfd7ae45cafaa5c`, package `chat@4.39.0`
|
||||
**OpenTag snapshot:** `6a770d862349f8e996c23c145aef6d6275914a23`
|
||||
|
||||
## Research question
|
||||
|
||||
How should Paperclip place its existing agents inside Slack and other external communication systems while preserving Paperclip tasks, runs, permissions, and governance as the source of truth?
|
||||
|
||||
## Paperclip baseline
|
||||
|
||||
Paperclip is already task/comment-centric rather than a generic chatbot. It has:
|
||||
|
||||
- company-scoped agents with independent runtime adapters;
|
||||
- single-assignee tasks, atomic checkout, wakeups, active-run handling, and liveness recovery;
|
||||
- users, agent keys, responsible-user attribution, permission grants, review policies, approvals, and budgets;
|
||||
- issue comments, typed interactions, documents, attachments, work products, and activity history;
|
||||
- Apps v2 connection, secret, identity, permission, review, test, and activity surfaces;
|
||||
- first-party Apps, route, job, settings, and UI infrastructure suitable for a native chat-adapters subsystem.
|
||||
|
||||
That means a channel integration should not create another agent runtime or conversational database. Its job is to translate external events into governed Paperclip task operations and translate safe Paperclip output back into the provider.
|
||||
|
||||
## Vercel Chat SDK
|
||||
|
||||
Sources: [repository](https://github.com/vercel/chat), [adapter catalog](https://chat-sdk.dev/adapters), [documentation](https://chat-sdk.dev/docs), [agent-readable index](https://chat-sdk.dev/llms.txt).
|
||||
|
||||
### What it contributes
|
||||
|
||||
- One TypeScript abstraction over mentions, subscribed messages, reactions, actions, slash commands, modals, messages, threads, cards, files, DMs, and ephemeral replies.
|
||||
- AI streaming that can select native Slack streaming, Telegram private-chat draft previews, or post/edit fallbacks.
|
||||
- Explicit overlapping-message policies: burst, queue, debounce, drop, or concurrent processing.
|
||||
- A static `chat/adapters` catalog containing package names, factory exports, peer dependencies, credential modes, required/optional environment variables, and secret annotations.
|
||||
- Pluggable state adapters for memory, Redis/ioredis, PostgreSQL, and vendor runtimes.
|
||||
|
||||
### Catalog snapshot
|
||||
|
||||
The pinned catalog includes official packages for Slack, Teams, Google Chat, Discord, GitHub, Linear, Notion, Telegram, WhatsApp Business Cloud, Twilio, X/XChat, Messenger, Instagram, and Web. It also lists vendor-official or community integrations for Liveblocks, Resend email, Sendblue/iMessage, Zernio, Matrix, Webex, WhatsApp bridges, Lark, Velt, Kapso, Novu, Linq, Photon, Dial, Weixin, LINE, and others.
|
||||
|
||||
The catalog should seed Paperclip setup metadata, but it is not a compatibility guarantee. Paperclip must maintain its own reviewed registry with pinned package/version, maturity, deployment compatibility, and feature-test results.
|
||||
|
||||
### What Paperclip must not delegate
|
||||
|
||||
Chat SDK's subscription, queue, lock, and history abstractions are bot-building conveniences. Paperclip needs stronger durable delivery, task binding, actor authorization, audit, and outbox semantics. A Paperclip state adapter should implement the SDK contract on Paperclip-owned records while leaving Paperclip's delivery ledger authoritative.
|
||||
|
||||
### Thread topology and GitHub
|
||||
|
||||
Chat SDK normalizes provider threads, but Paperclip must choose what a thread means. The selected model is Hermes-style for channel products: a root `@bot` mention on Slack, Discord, or a compatible Teams channel creates/opens a native provider thread; that thread owns exactly one Paperclip issue for the endpoint; all later conversation stays inside it without repeated mentions. This keeps the channel timeline readable and gives Paperclip a stable task boundary.
|
||||
|
||||
GitHub joins the first supported group through its official Chat SDK adapter. Its issue, pull-request, or discussion already is the native conversation thread, so an addressed comment binds that existing thread to one Paperclip issue instead of creating a second GitHub thread. Telegram uses a stable chat/topic boundary where nested threads are unavailable. These differences belong in adapter capabilities, not provider-name conditionals in orchestration code.
|
||||
|
||||
## OpenTag
|
||||
|
||||
Source: [CopilotKit/OpenTag](https://github.com/CopilotKit/OpenTag).
|
||||
|
||||
OpenTag is a complete Channels SDK starter rather than a general control plane. Its useful patterns are:
|
||||
|
||||
- a clear managed-versus-self-hosted channel runner boundary;
|
||||
- platform ingress separated from the long-running agent runtime by an outbound authenticated connection;
|
||||
- mention activates a thread, follow-ups in that subscribed thread continue, and unmentioned messages in a fresh conversation remain silent;
|
||||
- sender-aware context, file-aware prompts, rich native output, and resumable confirmation cards;
|
||||
- diagnostics that distinguish declared channel, platform setup, environment, runtime connectivity, and live delivery;
|
||||
- explicit warnings about competing runtimes claiming the same delivery identity.
|
||||
|
||||
OpenTag binds one visible persona (`AGENT_DISPLAY_NAME`) to one AG-UI agent URL. It does not solve Paperclip's company, multi-agent, task, permissions, budget, or audit model. Its managed Intelligence service owns provider credentials, delivery, state, and concurrency; Paperclip must own those controls itself or through an optional relay that does not become the business authority.
|
||||
|
||||
## Claude Tag
|
||||
|
||||
Source: [Introducing Claude Tag](https://www.anthropic.com/news/introducing-claude-tag).
|
||||
|
||||
Claude Tag presents one shared `@Claude` identity inside a selected Slack channel. People tag it with tasks; it breaks work into stages, uses connected tools/data/codebases, and replies in a Slack thread. Anthropic describes that channel identity as multiplayer: one Claude shares the channel context and conversation with everyone.
|
||||
|
||||
That is appropriate for a single product persona. It does not match Paperclip's core identity model, where a company has many independently configured agents with separate roles, runtimes, permissions, managers, and budgets. Paperclip should therefore expose each selected agent as its own provider bot identity. The shared unit is the channel, not a merged Paperclip agent.
|
||||
|
||||
## Slack Add to Slack
|
||||
|
||||
Source: [Slack's Add to Slack announcement](https://slack.com/blog/news/add-to-slack).
|
||||
|
||||
Slack describes Add to Slack as a standardized authorization and deployment bridge from agent builders into a workspace, with platform-handled multi-tenant permission scoping and centralized Slack governance. The examples emphasize individual agents with their own identities, permissions, and audit trails living beside teammates.
|
||||
|
||||
This validates a future managed provisioning path, but Paperclip cannot depend on it initially:
|
||||
|
||||
- it is Slack-specific while the architecture must cover many providers;
|
||||
- it simplifies installation, not Paperclip task/run/permission semantics;
|
||||
- provider workspace permission inheritance does not replace Paperclip authorization;
|
||||
- self-hosted Paperclip still needs BYO credentials and private-network relay options.
|
||||
|
||||
## One bot per agent versus shared bot
|
||||
|
||||
| Model | Strength | Failure in Paperclip | Decision |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
|
||||
| One shared bot dispatches to many agents | One installation and credential set | Hidden addressing grammar, ambiguous identity, mixed permissions/audit, unclear output ownership | Do not use for v1 |
|
||||
| One bot identity per Paperclip agent | Native addressing, visible role, clean task ownership, separate permissions and audit | More provider installations and credential lifecycle | Adopt |
|
||||
| One fixed product persona | Simple, Claude Tag/OpenTag-like experience | Does not expose the Paperclip company roster | Allow only as one ordinary Paperclip agent endpoint |
|
||||
|
||||
Within a shared Slack channel, `@Researcher` and `@Engineer` are separate apps. A root mention creates the native thread and one issue owned by the addressed endpoint; human replies in that thread continue it without another mention. If a second Paperclip bot participates through an explicit route, Paperclip records a separate related single-assignee issue and guarded route provenance rather than stealing or sharing the first issue.
|
||||
|
||||
## Provider shape taxonomy
|
||||
|
||||
The UI should not clone a wizard for every adapter or expose these patterns as onboarding steps. `/apps` renders one conditional purpose choice, the existing single-agent picker, and one provider handoff. The following taxonomy drives that final handoff and post-connect detail fields:
|
||||
|
||||
1. **Workspace app:** Slack, Teams, Google Chat, Discord, Lark. App registration, tenant/workspace selection, webhook/event subscriptions, scopes, and bot identity.
|
||||
2. **Comment system:** GitHub, Linear, Notion, Liveblocks, Velt. App/token plus repository/page/room scope; comments and mentions form threads.
|
||||
3. **Bot token:** Telegram and similar systems. Token, webhook secret/mode, group/channel allowlist, username.
|
||||
4. **Meta messaging:** WhatsApp, Messenger, Instagram, Kapso. Business/page/account identifiers, access/app/verify secrets, webhook registration, messaging windows/templates.
|
||||
5. **Phone/RCS/iMessage:** Twilio, Sendblue, Linq, Photon, AgentPhone. Sender number/identity, API credential, webhook, media/delivery restrictions.
|
||||
6. **Public social:** X/XChat. Bot account/OAuth, public mention and DM modes, media and rate-limit constraints.
|
||||
7. **Email:** Resend. From identity/domain, API/webhook secrets, threading headers, HTML/text and attachment behavior.
|
||||
8. **Web/embedded comments:** Web adapter and collaboration vendors. Host-supplied user authentication and conversation identity.
|
||||
|
||||
## Minimum-setup findings
|
||||
|
||||
- **Slack:** a customer-owned App created from a prepared manifest is the required first-release path; the operator installs it, then copies the Bot User OAuth Token and Signing Secret. Add to Slack remains an optional convenience when Paperclip participates in Slack's agent-deployment program and cannot gate release. Slack documents [shareable app-manifest URLs](https://docs.slack.dev/app-manifests/configuring-apps-with-app-manifests/) and the [install/token/signing-secret sequence](https://api.slack.com/tutorials/tracks/app-home-and-modals).
|
||||
- **GitHub:** a customer-owned GitHub App is the required path. Paperclip generates and stores the webhook secret, exposes it once for copying to GitHub, and then accepts the App ID and private-key PEM. Repository selection remains GitHub's installation step. The App Manifest exchange remains a possible future convenience, not a release dependency.
|
||||
- **Microsoft Teams:** the portable first-release path uses a customer-owned single-tenant Entra App, Azure Bot, and the three identity values entered in Paperclip. No provisioning helper is shipped or required. See the [Teams registration quickstart](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-sdk/get-started/quickstart-register).
|
||||
- **Discord:** the portable first-release path uses a customer-owned application bot with Application ID, Server ID, and write-only bot token. Paperclip generates the least-privilege server-pinned `bot` install URL and verifies Message Content Intent, server membership, and effective channel permissions. Discord's outbound Gateway transport requires no public callback or interactions key.
|
||||
- **Telegram:** BotFather's `/newbot` flow and returned token cannot be removed. Telegram bots also cannot initiate a conversation, so the smallest proof is: paste the token, open the bot, tap Start, and send one private message. See the [BotFather tutorial](https://core.telegram.org/bots/tutorial) and [Telegram bot introduction](https://core.telegram.org/bots).
|
||||
|
||||
These findings produce a strict UI rule: if an operator cannot act on information during the current setup phase, omit it. Automatic credential storage, transport selection, capabilities, and successful checks belong outside onboarding.
|
||||
|
||||
## Feature-to-Paperclip mapping
|
||||
|
||||
| Chat SDK feature | Paperclip source/target | Required guard |
|
||||
| -------------------------- | ------------------------------------- | ---------------------------------------------------- |
|
||||
| Mention/subscribed message | Task create/comment/wakeup | Endpoint/resource activation policy |
|
||||
| Reaction | Receipt or explicit reaction event | Self/loop suppression and capability check |
|
||||
| Streaming | Safe public run projection | No raw traces; rate/edit limits |
|
||||
| Card | Artifact, status, interaction, or URL | Safe renderer and text fallback |
|
||||
| Button/dropdown/modal | Typed interaction resolution | Current identity, resolver audience, exact once |
|
||||
| Slash command | Explicit channel command | Command allowlist and normal authorization |
|
||||
| File | Issue attachment/work product | Bounded download, type/hash/sanitize |
|
||||
| DM | Conversation-bound task | DM policy and stable provider identity |
|
||||
| Ephemeral reply | Denial/link/receipt | DM or safe normal-message fallback |
|
||||
| Overlapping messages | Comment queue or steer/new run | Paperclip task/run concurrency remains authoritative |
|
||||
|
||||
## Resulting recommendation
|
||||
|
||||
Adopt Chat SDK below a native Paperclip channel control plane. Reuse the current `/apps` catalog, connection wizard shell, single-agent selector, and connector-detail navigation. Onboarding asks only for purpose when ambiguous, the agent, and the provider invite/handoff; reviewed defaults create the endpoint, while Channels, Access, Behavior, Conversations, and Activity remain editable afterward. Implement chat adapters directly in Paperclip with a Paperclip-backed Chat SDK state adapter, durable ingress/outbox, provider-thread provisioning, explicit identity linking, sponsored restricted guests, and endpoint-bound issues. Begin with Slack, Teams, Discord, Telegram, and GitHub, but generate setup and capability UI from a reviewed adapter registry so every later adapter is an enablement exercise rather than an architectural fork.
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
# Chat Adapters UI Surface Specification
|
||||
|
||||
**Status:** historical v1 requirements inventory; current product flow is `2026-09-04-chat-adapters-ui-surfaces-v8.md`. Managed-install and helper-first concepts below are not shipped requirements.
|
||||
**Date:** 2026-09-03
|
||||
**Paperclip base:** `origin/master` at `8430bd897f01dd4b91e0970efffb71b97e5a2685` (refreshed from earlier planning references through `d593463ab6394cd356bf27448ea28bad8cccf4ec`)
|
||||
**Historical wireframes:** see the [Git archive](./wireframes-archive.md). The 19-screen inventory below is retained as architecture-coverage history, not the proposed onboarding flow. Generated images are excluded from the PR.
|
||||
**Archived wireframes:** [v1 SVG snapshot](https://github.com/paperclipai/paperclip/tree/1c4a45f0ef7d627aa98e4f3ae3116d4507386d1a/doc/plans/chat-adapters/wireframes) ([archive and regeneration notes](./wireframes-archive.md))
|
||||
|
||||
## 1. Information architecture
|
||||
|
||||
Channel integrations extend existing Paperclip surfaces rather than adding a new global product area.
|
||||
|
||||
- **Apps / Connectors** remains discovery and connection management.
|
||||
- Chat adapters are a native Paperclip subsystem surfaced through Apps; they are not installed or managed as a plugin.
|
||||
- A provider with channel support exposes a clearly separate **Talk to an agent here** connection method beside any **Let agents use this app** tool method.
|
||||
- A channel connection reuses the App detail shell with `Overview`, `Access`, `Behavior`, `Conversations`, and `Activity` views.
|
||||
- Agent detail adds **Channels** under Runtime, between Tools and Governance.
|
||||
- Task detail adds a channel-source banner, external actor attribution, outbound-publication state, and detach controls only when bound.
|
||||
- Identity linking uses a minimal public Paperclip route reached from an ephemeral provider message or DM.
|
||||
- Private self-host relay configuration lives inside the channel endpoint; it is not a global infrastructure page.
|
||||
- Slack, Teams, Discord, Telegram, and GitHub are the initial supported set. Adapter capabilities determine whether Paperclip creates a native thread, binds an existing thread, or uses the stable conversation as the issue boundary.
|
||||
|
||||
The default audience is a company operator connecting and governing an agent. External participants see their native provider, not these configuration screens.
|
||||
|
||||
## 2. Cross-surface rules
|
||||
|
||||
- Always name the selected Paperclip agent and provider bot identity together.
|
||||
- Always distinguish tool access from chat presence.
|
||||
- State who can trigger the agent, where, and as which Paperclip principal before activation.
|
||||
- Describe effective permissions; never imply that a provider membership grants Paperclip authority.
|
||||
- Put safe defaults first: root mention creates/opens a provider thread and one Paperclip issue, threaded replies continue without mentions, queued overlap, public milestones only, linked-user permissions, sponsored restricted guests, agent routes off.
|
||||
- Hide unsupported configuration and show the provider fallback beside partially supported behavior.
|
||||
- Never display secrets after save. Show secret labels, source, last rotation, and health only.
|
||||
- Every failed setup or delivery state says what happened, whether work was accepted, and the next safe action.
|
||||
- Desktop uses the existing Paperclip primary and contextual sidebars. Mobile uses the existing drawer/header pattern with one full-width content column and 48px actions.
|
||||
|
||||
## 3. Screen specifications and annotations
|
||||
|
||||
### 01 — Connectors catalog
|
||||
|
||||
Purpose: discover providers and see whether each is connected for tools, channels, or both.
|
||||
|
||||
1. Existing Apps contextual navigation remains the entry point.
|
||||
2. Filter chips select All, Tools, Channels, or Connected; search remains provider-wide.
|
||||
3. Provider rows show separate tool/channel status and endpoint count.
|
||||
4. Maturity and deployment badges prevent unsupported adapters from looking connectable.
|
||||
5. Primary action opens provider detail; mobile keeps filters horizontally scrollable and rows stacked.
|
||||
|
||||
### 02 — Connection method
|
||||
|
||||
Purpose: make directionality unambiguous before credentials are requested.
|
||||
|
||||
1. Provider header and current accounts preserve Apps context.
|
||||
2. Tool method explains the agent calls the provider as an external tool.
|
||||
3. Channel method explains people message one selected Paperclip agent.
|
||||
4. Identity/credential warning states that the methods are independently governed.
|
||||
5. Continue is attached to the selected method; mobile cards become a vertical radio list.
|
||||
|
||||
### 03 — Choose agent and bot identity
|
||||
|
||||
Purpose: establish the endpoint's permanent Paperclip owner.
|
||||
|
||||
1. Wizard progress names the current step and retains a safe exit.
|
||||
2. Agent selector shows active/invokable agents and their roles.
|
||||
3. Native bot preview derives name/avatar from the agent and shows provider identity constraints.
|
||||
4. One-bot-per-agent explanation shows how multiple agents coexist in one channel.
|
||||
5. Collision/inactive-agent warnings block continuation; mobile preview follows the selector.
|
||||
|
||||
### 04 — Provider installation
|
||||
|
||||
Purpose: connect a real provider installation without hiding manual work.
|
||||
|
||||
1. Setup pattern switcher demonstrates Slack while allowing adapter-generated instructions.
|
||||
2. BYO setup checklist exposes manifest/app creation, scopes, webhook URL, and event subscription.
|
||||
3. Credentials are secret-reference fields with masking and source labels.
|
||||
4. Verification checks signature, bot identity, scopes, and reachability independently.
|
||||
5. Managed install is visibly optional/unavailable and never blocks BYO continuation.
|
||||
|
||||
### 05 — Conversation reach
|
||||
|
||||
Purpose: constrain where the bot can listen and explain root-mention-to-thread activation.
|
||||
|
||||
1. Workspace/tenant identity is read-only after verification.
|
||||
2. Resource allowlist supports discovery plus exact external ids.
|
||||
3. On Slack, Discord, and compatible Teams channels, a root mention creates/opens a native thread and exactly one endpoint-owned Paperclip issue; later thread replies need no mention.
|
||||
4. DM policy explains its task boundary and proactive-DM restriction.
|
||||
5. Example panel shows root mention, bot-created thread, threaded follow-up, and ignored fresh root message; mobile puts it in a disclosure.
|
||||
|
||||
### 06 — People and permissions
|
||||
|
||||
Purpose: establish external-to-Paperclip authority before activation.
|
||||
|
||||
1. Endpoint sponsor selection explains why a sponsor is required.
|
||||
2. Linked-user path maps a provider principal to one Paperclip user after confirmation.
|
||||
3. Unlinked-user path shows the restricted guest profile and allowed operations.
|
||||
4. Effective-authority formula visibly intersects sponsor, resource, guest, and target controls.
|
||||
5. Governance actions are explicitly denied to guests; mobile presents the formula as ordered rows.
|
||||
|
||||
### 07 — Output and interaction behavior
|
||||
|
||||
Purpose: choose what the bot exposes and how it behaves across provider capabilities.
|
||||
|
||||
1. Acknowledgement policy selects reaction, ephemeral, or short-message fallback.
|
||||
2. Progress policy exposes safe milestones and update cadence, never reasoning traces.
|
||||
3. Output controls cover final text, artifacts, cards, actions, modals, files, and URLs.
|
||||
4. Command/reaction/edit/delete behavior is capability-aware.
|
||||
5. Concurrency selects queue by default plus burst, debounce, drop, or concurrent modes.
|
||||
|
||||
### 08 — Agent-to-agent routes
|
||||
|
||||
Purpose: make bot-to-bot participation an explicit governed exception.
|
||||
|
||||
1. Master control is off by default and explains the risk.
|
||||
2. Directed route chooses a source endpoint, destination endpoint, and permitted resources.
|
||||
3. Trigger and maximum-hop controls limit when a bot message activates another agent.
|
||||
4. Loop-protection summary lists self-message, revisit, fingerprint, and hop suppression.
|
||||
5. Audit preview shows what route provenance is retained.
|
||||
|
||||
### 09 — Review and activate
|
||||
|
||||
Purpose: provide one comprehensible safety review and a real delivery test.
|
||||
|
||||
1. Readback names agent, bot, workspace, resources, people policy, and behavior.
|
||||
2. Provider checks distinguish credential, signature, webhook, scope, and bot-membership health.
|
||||
3. Test message instructions verify root mention, provider-thread creation, one Paperclip issue, unmentioned threaded follow-up, and fresh-root silence.
|
||||
4. Activation control remains disabled until required checks pass.
|
||||
5. Managed provisioning notice is informational; BYO completion is sufficient.
|
||||
|
||||
### 10 — Endpoint overview
|
||||
|
||||
Purpose: answer what is connected, whether it works, and what the operator can do.
|
||||
|
||||
1. Header binds agent identity, bot identity, provider installation, and endpoint status.
|
||||
2. Health summary shows provider, ingress/relay, credentials, and last delivery separately.
|
||||
3. Activity summary counts conversations, active tasks, failed deliveries, and linked people.
|
||||
4. Test, pause/resume, reconnect, and open-provider actions are available near status.
|
||||
5. Remove lives in a distinct danger section and describes task/history retention.
|
||||
|
||||
### 11 — Endpoint access
|
||||
|
||||
Purpose: manage reachable resources and external identities after setup.
|
||||
|
||||
1. Resource allowlist supports enable/disable and verification state.
|
||||
2. Principal table distinguishes linked user, sponsored guest, bot, revoked, and unknown.
|
||||
3. Link intent produces a one-time URL without exposing credentials.
|
||||
4. Sponsor and guest profile changes show their effective impact before save.
|
||||
5. Revocation stops future user attribution but preserves historical audit identity.
|
||||
|
||||
### 12 — Endpoint behavior
|
||||
|
||||
Purpose: edit the policies chosen during setup with provider fallbacks visible.
|
||||
|
||||
1. Activation, provider-thread creation mode, existing-thread binding, and DM policies are grouped by inbound behavior.
|
||||
2. Queue/overlap policy names the Paperclip run consequence.
|
||||
3. Progress, streaming, and publication settings are grouped by outbound behavior.
|
||||
4. Files/interactions/commands/reactions show supported, fallback, or unavailable states.
|
||||
5. Save creates a versioned policy and previews material changes.
|
||||
|
||||
### 13 — Conversations and tasks
|
||||
|
||||
Purpose: inspect the external-thread-to-issue binding ledger and prove the one-thread/one-issue invariant.
|
||||
|
||||
1. Rows show provider resource/thread, exactly one endpoint-owned Paperclip issue, participant count, subscription, and activity.
|
||||
2. Filters cover active, waiting, failed, detached, and DM conversations.
|
||||
3. Selection opens a detail panel with provider and Paperclip backlinks.
|
||||
4. Detach explains that history remains and future messages may create a new task.
|
||||
5. Agent assignment is visible but not editable while bound.
|
||||
|
||||
### 14 — Deliveries and diagnostics
|
||||
|
||||
Purpose: make ingress/publication failures operable without exposing sensitive payloads.
|
||||
|
||||
1. Unified ledger filters inbound, outbound, actions, retries, ignored, and failures.
|
||||
2. Each row shows event kind, thread/task, state, attempt, timing, and dedupe result.
|
||||
3. Detail drawer contains redacted normalized fields, provider ids, leases, and error/remediation.
|
||||
4. Replay is authorized, idempotent, and unavailable for successfully applied mutations.
|
||||
5. Provider rate limit and relay/ingress health sit above the ledger.
|
||||
|
||||
### 15 — Agent Channels view
|
||||
|
||||
Purpose: see everywhere a particular Paperclip agent can be reached.
|
||||
|
||||
1. Agent contextual navigation adds Channels under Runtime.
|
||||
2. Endpoint cards show provider bot identity, workspace/resources, health, and trigger policy.
|
||||
3. Recent externally created tasks link into normal task detail.
|
||||
4. Add channel starts Apps setup with this agent preselected.
|
||||
5. Empty state explains that the agent still works normally inside Paperclip.
|
||||
|
||||
### 16 — Externally bound task
|
||||
|
||||
Purpose: preserve normal task work while making channel ownership and publication explicit.
|
||||
|
||||
1. Source banner links to provider conversation and endpoint and explains the assignment lock.
|
||||
2. External participant comments use provider attribution without impersonating a Paperclip user.
|
||||
3. Agent output shows queued/streaming/delivered/failed publication state.
|
||||
4. Board composer defaults to internal; **Send to channel** is an explicit option with preview.
|
||||
5. Assignee control is locked until detach; the confirmation preserves history and warns about future messages.
|
||||
|
||||
### 17 — Identity-link flow
|
||||
|
||||
Purpose: safely map one provider principal to the currently authenticated Paperclip user.
|
||||
|
||||
1. Landing page shows provider identity, bot/endpoint, company, and expiration.
|
||||
2. Authentication is required before confirmation and returns to the same intent.
|
||||
3. Confirmation names both identities; no email-based auto-linking occurs.
|
||||
4. Success explains that future actions use current Paperclip permissions.
|
||||
5. Expired, used, revoked, company-mismatch, and wrong-account states provide safe remediation.
|
||||
|
||||
### 18 — Self-hosted relay
|
||||
|
||||
Purpose: let private instances receive provider events without becoming publicly reachable.
|
||||
|
||||
1. Direct and relay modes are compared with current reachability detection.
|
||||
2. Relay enrollment shows a redacted command/config and a one-time secret handoff.
|
||||
3. Health shows connection owner, heartbeat, backlog, last delivery, and provider verification.
|
||||
4. Key rotation and revoke controls explain connection interruption.
|
||||
5. Offline/degraded states distinguish provider acceptance from Paperclip processing.
|
||||
|
||||
### 19 — Adapter and state matrix
|
||||
|
||||
Purpose: prove the design generalizes beyond Slack and specify shared empty/error language.
|
||||
|
||||
1. Provider taxonomy covers workspace apps, comment systems, bot tokens, Meta messaging, phone/iMessage, public social, email, and embedded web.
|
||||
2. Capability columns cover mentions/messages, stream/edit, cards/actions/modals, commands, emoji, files, DMs, and ephemeral responses.
|
||||
3. Setup patterns show which fields are generated from the reviewed adapter registry.
|
||||
4. Maturity states are experimental, preview, stable, unavailable, and revoked.
|
||||
5. UI states cover loading, empty, degraded, permission denied, unsupported fallback, rate limited, and dead letter.
|
||||
|
||||
## 4. Flow map
|
||||
|
||||
`wireframes/flow.svg` connects discovery, method choice, the seven setup decisions, activation, endpoint management, agent view, task view, identity linking, relay setup, diagnostics, detach, and rebind. Solid arrows represent the primary operator path; dashed arrows represent identity, relay, failure, and detach branches.
|
||||
|
||||
## 5. Copy and state defaults
|
||||
|
||||
- Use **channel connection** for the Paperclip configuration and **bot identity** for the provider-visible account.
|
||||
- Use **external participant** for an unlinked provider human and **linked user** after confirmation.
|
||||
- Use **sponsored guest** only in permission explanations, not as the person's display name.
|
||||
- Default activation on thread-capable channels: “Mention this agent in the channel. It opens a thread and one Paperclip issue; continue in that thread without mentioning it again.”
|
||||
- Existing-thread activation: “Mention this agent in a GitHub issue, pull request, discussion, or another supported existing thread. That thread binds to one Paperclip issue.”
|
||||
- Conversation fallback: “This provider has no nested threads; this chat or topic is the Paperclip issue boundary.”
|
||||
- Default overlap: “Queue messages on this task.”
|
||||
- Default publication: acknowledgement, coarse safe milestones, final agent output, approved artifacts, and interactions; no reasoning trace.
|
||||
- Default board composer label: “Internal note”; explicit alternate: “Send to channel.”
|
||||
- Assignment denial: “This task belongs to the channel connection for {agent}. Detach it before assigning another agent.”
|
||||
- Guest governance denial: “Link your Paperclip account and use an authorized user, or open this action in Paperclip.”
|
||||
- Unsupported feature: name the text/link fallback rather than only saying “unsupported.”
|
||||
|
||||
## 6. Responsive and accessibility requirements
|
||||
|
||||
- Desktop wires use the current Paperclip global/contextual sidebar structure and preserve scanning density.
|
||||
- Mobile wires use a 375×812 canvas, 16px outer margin, a 48px header/action rhythm, and one content column.
|
||||
- Tables become stacked summary rows or cards; detail drawers become full-height sheets.
|
||||
- Wizard steps use a compact progress label rather than a horizontally clipped stepper.
|
||||
- All key state is expressed in text, not color.
|
||||
- Annotation red is review-only and not part of the proposed UI.
|
||||
- Provider icons are grayscale placeholders with visible text labels.
|
||||
- Long ids, timestamps, delivery ids, and secret labels use the eventual machine-value style; wireframes abbreviate them without presenting real secrets.
|
||||
|
||||
## 7. Acceptance matrix
|
||||
|
||||
Every architecture capability has a visible place:
|
||||
|
||||
- discovery/directional choice: 01–02;
|
||||
- endpoint identity/setup: 03–04;
|
||||
- resource, identity, and permission configuration: 05–06, 11;
|
||||
- complete Chat SDK behavior set: 07, 12, 19;
|
||||
- agent routing: 08;
|
||||
- verification and lifecycle: 09–10;
|
||||
- conversation/task binding and publication: 13, 15–16;
|
||||
- durable delivery operations: 14;
|
||||
- explicit identity linking: 17;
|
||||
- private self-host deployment: 18;
|
||||
- provider differences and edge states: 19.
|
||||
|
||||
The initial launch matrix is Slack, Microsoft Teams, Discord, Telegram, and GitHub. Slack and Discord use root-mention thread creation; Teams uses that mode on channel surfaces with stable post/reply threads; GitHub binds an existing issue/PR/discussion thread; Telegram uses the stable chat/topic boundary.
|
||||
|
||||
No external provider client is wireframed: those products own their UI. The package specifies the Paperclip surfaces and describes provider-visible behavior in annotations and examples.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,112 @@
|
|||
# Paperclip Chat Adapters — Minimum Setup v6
|
||||
|
||||
Date: 2026-09-04
|
||||
Paperclip base: `8430bd897f01dd4b91e0970efffb71b97e5a2685`
|
||||
Historical viewer: [Git archive](./wireframes-archive.md).
|
||||
Archived wireframes: [v6 SVG snapshot](https://github.com/paperclipai/paperclip/tree/1c4a45f0ef7d627aa98e4f3ae3116d4507386d1a/doc/plans/chat-adapters/wireframes-v6) ([archive and regeneration notes](./wireframes-archive.md))
|
||||
|
||||
## Relevance rule
|
||||
|
||||
A setup screen may show only something the operator must do during that step:
|
||||
|
||||
- click a Paperclip or provider action;
|
||||
- choose something in the provider's UI;
|
||||
- copy, paste, or upload a required value;
|
||||
- run a required command;
|
||||
- send the message that verifies the connection.
|
||||
|
||||
Do not show the selected agent again after selection. Do not show automatic credential storage, delivery selection, capability lists, successful checks, resource inventories, or explanatory status rows. Those belong in implementation, Activity diagnostics, or contextual repair states. Show errors and missing prerequisites only when they occur.
|
||||
|
||||
The persistent step rail is sufficient context. **Save & exit** preserves the draft. Completing the real provider test activates the connection and treats the explicitly tested destination as its first enabled resource. Any channel, chat, topic, or repository discovered later starts disabled until a Paperclip administrator enables it in Settings.
|
||||
|
||||
## Slack
|
||||
|
||||
### Required path: customer-owned Slack App
|
||||
|
||||
1. **Create and install:** Paperclip opens Slack's official app-from-manifest URL. In Slack, choose the workspace, review the manifest, create the App, and install it.
|
||||
2. **Connect:** copy **Bot User OAuth Token** from **OAuth & Permissions** and **Signing Secret** from **Basic Information → App Credentials**; paste those two values into Paperclip.
|
||||
3. **Try Maya:** open a channel, use `/invite @Maya` if required, post `@Maya help me test this` as a new channel message, and reply once in Maya's thread.
|
||||
|
||||
The tested Slack channel is enabled when the test succeeds. Inviting Maya to another channel later only makes it available; Paperclip remains silent there until an administrator enables that channel in Settings.
|
||||
|
||||
The prepared manifest contains Maya's app identity, callback URLs, least-privilege bot scopes, event subscriptions, interactivity, commands, and file behavior. The operator does not configure those individually. Slack documents [shared manifest URLs](https://docs.slack.dev/app-manifests/configuring-apps-with-app-manifests/) and the [install/token/signing-secret locations](https://api.slack.com/tutorials/tracks/app-home-and-modals).
|
||||
|
||||
### Non-shipped future convenience
|
||||
|
||||
An **Add to Slack** flow may be added later. It is not shipped, is not shown as a current setup option, and cannot gate the first release or replace the customer-owned App path.
|
||||
|
||||
## GitHub
|
||||
|
||||
### Required customer-owned GitHub App path
|
||||
|
||||
1. **Create GitHub App:** copy Paperclip's webhook URL, click **Generate webhook secret**, then create a GitHub App with those values, **Issues: write**, **Pull requests: write**, **Metadata: read**, and the selectable issue/review-comment events. GitHub supplies installation lifecycle events automatically.
|
||||
2. **Choose repositories:** click **Install in GitHub**, choose the account or organization, choose all or selected repositories, review permissions, and install.
|
||||
3. **Try Maya:** open an issue or pull request in an installed repository, comment `@paperclip-maya help me test this`, then add another comment to continue the same Paperclip task.
|
||||
|
||||
The tested repository is enabled when the test succeeds. Any other repository in the App installation remains disabled in Paperclip until enabled in Settings.
|
||||
|
||||
Paperclip returns the webhook secret only once and never exposes it from normal endpoint reads. After GitHub creates the App, the operator enters the App ID and private-key PEM; Paperclip verifies the App permissions and subscribed events before retaining the credentials.
|
||||
|
||||
### Existing GitHub App
|
||||
|
||||
1. Copy Paperclip's generated webhook URL and one-time secret into the existing GitHub App and make the webhook active. Regenerating rotates the stored secret and requires updating GitHub before further deliveries can verify.
|
||||
2. Grant **Issues: write**, **Pull requests: write**, and **Metadata: read**; subscribe to **Issue comment** and **Pull request review comment**.
|
||||
3. Generate a private key in the App settings.
|
||||
4. Paste the App ID and upload the PEM file to Paperclip, then connect and verify.
|
||||
5. Continue through GitHub's ordinary repository-installation and test steps.
|
||||
|
||||
The webhook secret is generated and already stored by Paperclip; it is copied outward rather than requested back from GitHub.
|
||||
|
||||
## Microsoft Teams
|
||||
|
||||
### Required customer-owned bot path
|
||||
|
||||
1. Copy Paperclip's messaging endpoint.
|
||||
2. Create a single-tenant Entra App registration and client secret, then create an Azure Bot using the Application ID, enable its Microsoft Teams channel, and set Paperclip's messaging endpoint.
|
||||
3. In Teams Developer Portal, create the customer-owned Teams app, add the same bot Application ID for Personal, Team, and Group chat scopes, apply the required resource-specific consent entries, and publish or download/upload that app according to tenant policy.
|
||||
4. Paste the Application ID, Directory/Tenant ID, and client-secret value into Paperclip, then install the customer-owned app in the intended scope.
|
||||
5. **Try Maya:** open an installed channel, start a new post, send `@Maya help me test this`, and reply once beneath the post.
|
||||
|
||||
The tested Teams channel is enabled when the test succeeds. Installing Maya into another team or channel later makes that destination available but does not enable Paperclip work there.
|
||||
|
||||
No provisioning helper is part of the shipped path. See the [Teams registration quickstart](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-sdk/get-started/quickstart-register).
|
||||
|
||||
If tenant policy requires administrator approval, Microsoft owns that state inside the same install step. Paperclip preserves the draft; it does not add another configuration page.
|
||||
|
||||
Those three identity values are the minimum portable credentials for the manual customer-owned registration. Paperclip does not show authentication-strategy, cloud, webhook, relay, package, scope, or capability choices on the normal path. For tenants that require package submission rather than direct sideloading, Microsoft's publication or installation flow may return an administrator-approval state; Microsoft documents the [custom-app upload and approval paths](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload).
|
||||
|
||||
## Discord
|
||||
|
||||
### Required customer-owned bot path
|
||||
|
||||
1. **Create bot:** open Discord Developer Portal and create one application dedicated to the selected immutable Paperclip agent. Copy its Application ID, add a bot, enable **Message Content Intent**, and paste the bot token only into Paperclip's masked field.
|
||||
2. **Choose server:** enter the Server ID for the authorized test server. Inspect Paperclip's generated install URL, which must request only the `bot` scope and permission integer `309237763136`; install it in that server without Administrator or `applications.commands`.
|
||||
3. **Connect:** Paperclip verifies that the token belongs to the Application ID, Message Content is enabled, the bot is installed in the stated server, and at least one text channel has the required effective permissions.
|
||||
4. **Try Maya:** enable one discovered channel in Access, post `@Maya help me test this` as a new channel message, and reply once in the public thread Maya creates.
|
||||
|
||||
Discord uses a direct outbound Gateway connection, so setup does not ask for a public webhook URL, interactions public key, slash-command registration, or delivery choice. Other visible channels remain disabled until explicitly enabled in Paperclip; the direct-message reach switch remains off until an operator enables it.
|
||||
|
||||
This customer-owned bot path is the complete first-release setup. There is no managed Discord provisioning path in the current product.
|
||||
|
||||
## Telegram
|
||||
|
||||
1. **Create bot:** open BotFather, send `/newbot`, enter Maya's display name, choose an available username ending in `bot`, and paste the returned token into Paperclip.
|
||||
2. **Try Maya:** open the new bot's private chat, tap **Start**, and send `Help me test this`.
|
||||
|
||||
The successful private-chat test enables direct messages when setup completes. Groups and forum topics discovered later remain disabled until enabled in Settings.
|
||||
|
||||
Telegram has no bot-installation OAuth callback, so the BotFather token is the single unavoidable input. Telegram bots also cannot initiate a conversation; the person must start the bot or add it to a group. See Telegram's [BotFather tutorial](https://core.telegram.org/bots/tutorial) and [bot introduction](https://core.telegram.org/bots).
|
||||
|
||||
Group and forum installation is deliberately post-connect configuration. The minimum setup proves a working bot through a private message; an operator can later add the bot to a group and enable the discovered chat in connector Settings. Access remains reserved for external-identity linking and the unlinked-participation policy.
|
||||
|
||||
## Resulting screen inventory
|
||||
|
||||
| Provider | Normal setup screens | Alternate shipped path |
|
||||
| --------------- | ------------------------------------------------------------------------------------------: | ------------------------------------------ |
|
||||
| Slack | Create/install custom App; copy two secrets; Try Maya | None |
|
||||
| GitHub | Generate secret; configure App; App ID/private key; choose repositories; Try Maya | Existing App uses the same credential path |
|
||||
| Microsoft Teams | Manual Entra/Azure Bot and Teams app registration; three identity values; install; Try Maya | None |
|
||||
| Discord | Create bot; Application ID/token/Server ID; install; connect; Try Maya | None |
|
||||
| Telegram | BotFather token; Try Maya | None |
|
||||
|
||||
The linked v6 viewer predates the Discord implementation and remains a four-provider design artifact. The current product and acceptance contract cover all five providers; Discord uses the same Settings, Access, Conversations, and Activity tabs, with the setup path above. The read-only Overview and non-product interaction-walkthrough pages remain absent.
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
# Chat Adapters — Platform-specific Surfaces
|
||||
|
||||
**Status:** detailed wireframe companion
|
||||
**Date:** 2026-09-04
|
||||
**Paperclip base:** `origin/master` at `8430bd897f01dd4b91e0970efffb71b97e5a2685`
|
||||
**Chat SDK snapshot:** `51322dde8f4aafd8a7fc7a20cbfd7ae45cafaa5c`
|
||||
**Historical viewer and generated wireframes:** [Git archive](./wireframes-archive.md); images are excluded from the PR. Discord is the current implementation addendum below.
|
||||
**Current UI companion:** `2026-09-04-chat-adapters-ui-surfaces-v8.md`
|
||||
**Minimum setup specification:** `2026-09-04-chat-adapters-minimum-setup-v6.md`
|
||||
**Live browser acceptance:** `2026-09-04-chat-adapters-browser-e2e-runbook.md`
|
||||
|
||||
## 1. Shared frame, provider-owned differences
|
||||
|
||||
The shared product flow remains deliberately small:
|
||||
|
||||
`/apps` → purpose only for a dual-purpose registry entry → choose one immutable agent → provider step-rail wizard → connected.
|
||||
|
||||
The provider handoff may have several resumable phases because Slack, GitHub, Microsoft Teams, Discord, and Telegram require different external actions. Each setup page shows only things the operator must click, choose, copy, paste, upload, run, or perform at the provider. The page body never repeats the selected agent and never describes Paperclip's automatic work or successful checks. Errors and missing prerequisites appear only when they occur.
|
||||
|
||||
After connection, the existing connector detail shell provides provider-specific **Settings**, **Access**, **Conversations**, and **Activity** tabs. The read-only Overview tab is removed. Settings contains only destination reach that an operator can plausibly change. Task boundaries, provider identities, delivery, credentials, installation drift, and response capabilities are product behavior or contextual Activity repairs—not settings.
|
||||
|
||||
The runtime always uses the maximum safe provider capability set. Reactions, streaming, rich messages/cards, buttons, modals, commands, files, edits, DMs, and private-response fallbacks are not per-endpoint feature toggles. Availability is negotiated from the pinned adapter, provider installation and permission health, conversation type, safe-publication policy, and current Paperclip authorization. In the first wave, agent-authored questions and confirmations may degrade to actionless text plus a Paperclip URL when a safe native control is unavailable. Richer Paperclip governance interactions remain Paperclip-only until their complete partial-resolution, terminal-settlement, and recovery semantics are implemented; the connector never emits a provider card it cannot later settle.
|
||||
|
||||
The current setup wireframes use the supplied reference image only for its persistent step rail, completed checkmarks, one active phase, and bottom actions. They do not copy its text or function. Provider settings remain ordinary full-width vertical sections and rows. Provider-native interaction models remain behavioral documentation below; the former standalone walkthrough screens are removed because they are not product pages.
|
||||
|
||||
### Shared reach and access model
|
||||
|
||||
The provider and Paperclip enforce different layers:
|
||||
|
||||
1. **Provider availability ceiling:** Slack/Teams/Telegram/Discord decide where the bot is installed or invited; a GitHub App installation decides which repositories are available. Provider permissions and membership determine which events can reach Paperclip at all.
|
||||
2. **Paperclip resource enablement:** a Paperclip administrator enables a subset of those available channels, chats, topics, or repositories in Settings. An invitation alone is not authorization to create or continue a task.
|
||||
3. **Actor authorization:** after resource enforcement, a linked identity acts as its current Paperclip user. If the Access toggle allows unlinked people, they receive only the fixed restricted external profile and cannot approve, change budgets, hire, manage permissions/connections, or reassign agents.
|
||||
|
||||
The successful setup-test destination becomes the first enabled resource. Newly discovered provider resources appear disabled until explicitly enabled. Provider removal makes a resource unavailable and blocks new work without erasing its tasks or conversation history. Settings therefore answers **where may this bot work?** Access answers **who does this external person represent, and what authority applies?**
|
||||
|
||||
Conversations is only a cross-link list. Every row shows the external conversation, Paperclip task, current state, **Open provider**, and **Open task**. There are no binding actions, detach control, detached section, or task-boundary explainer.
|
||||
|
||||
| Platform | External install object | Default conversation boundary | Default activation | Output shape |
|
||||
| --------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| Slack | Slack app installed to workspace/Grid org | Root message's Slack thread; stable DM conversation | Root `@bot`; replies continue in bound thread | Native stream or post/edit, Block Kit, files, actions, modals, ephemeral |
|
||||
| GitHub | GitHub App installation on selected repositories | Existing issue, PR conversation, or inline review thread | `@bot` comment in allowed object | GFM comment/reaction/edit; links for files and governed actions |
|
||||
| Microsoft Teams | Entra/bot registration plus customer-owned Teams app installed to scope | Channel post/replies; stable DM or group-chat conversation | Direct mention by default | Post/edit output; Adaptive Cards/task modules; authenticated file links |
|
||||
| Discord | Discord application bot installed in one server | Created public thread; stable DM conversation | Root `@bot`; replies continue in bound thread | Post/edit, embeds, buttons, reactions, native files |
|
||||
| Telegram | BotFather bot token plus chat membership | Active DM/group binding or forum topic | DM message; group `@bot` or reply to bot | Throttled post/edit, optional DM drafts, inline buttons, media |
|
||||
|
||||
## 2. Slack
|
||||
|
||||
The [pinned Chat SDK Slack adapter](https://github.com/vercel/chat/blob/51322dde8f4aafd8a7fc7a20cbfd7ae45cafaa5c/packages/adapter-slack/README.md) supports single-workspace tokens, multi-workspace OAuth, Enterprise Grid, webhook and Socket Mode ingress, Block Kit interactions, files, DMs, ephemeral replies, and native streaming. Slack independently requires signed-request validation and prompt acknowledgement of [Events API](https://docs.slack.dev/apis/events-api/) and [interactive](https://docs.slack.dev/interactivity/handling-user-interaction/) payloads.
|
||||
|
||||
### Setup and external handoff — screen 13
|
||||
|
||||
The required customer-owned-App path has two Paperclip screens:
|
||||
|
||||
1. **Connect Slack app:** open Slack's app-from-manifest flow, create and install the prepared customer-owned App, then enter its Bot User OAuth Token and Signing Secret write-only in Paperclip.
|
||||
2. **Try Maya:** open a channel, invite Maya if Slack asks, post a root `@Maya` test message, and reply once in Maya's new thread.
|
||||
|
||||
The prepared-App flow contains only required work:
|
||||
|
||||
1. Open Slack's app-from-manifest URL, choose the workspace, create the prepared App, then install it from **OAuth & Permissions**.
|
||||
2. Copy **Bot User OAuth Token** and **Signing Secret** from the documented Slack settings locations and paste those two values into Paperclip.
|
||||
3. Converge on the same channel mention/thread-reply test.
|
||||
|
||||
A managed **Add to Slack** authorization flow is an optional future convenience. It is not shipped, cannot replace the customer-owned-App path, and cannot gate release.
|
||||
|
||||
Direct callback versus relay is selected automatically from instance reachability. Socket Mode is removed from endpoint onboarding and exists only as an instance-admin escape hatch when neither a callback nor relay is available. See the minimum-setup specification for the exact effect behind every button.
|
||||
|
||||
### Post-connect settings — screen 14
|
||||
|
||||
- **Channels:** list channels where the installed bot is already a member and let a Paperclip admin enable or disable each one. The workspace cannot change and appears only as context in channel labels. A later Slack invitation makes a channel available but leaves it disabled until enabled here.
|
||||
- **Add Maya to another Slack channel:** opens the provider instructions; it changes Slack membership, not Paperclip enablement.
|
||||
- **Allow direct messages:** one on/off toggle.
|
||||
- **Fixed behavior:** a root mention creates a Slack thread and one Paperclip task. Replies in that thread continue the task without another mention. The first mention in an existing unbound thread binds that thread without importing earlier history. Fresh unmentioned roots are ignored.
|
||||
- **Activity repairs:** invalid tokens, missing membership, revoked OAuth, or scope drift appear with a contextual reconnect, invite, or reinstall action only when the condition exists.
|
||||
|
||||
Delivery transport, credential rotation, installation drift, task boundaries, receipts, progress, streaming/post-edit output, Block Kit, actions, modals, commands, files, and ephemeral fallbacks do not appear in Settings.
|
||||
|
||||
### Runtime interaction model (not a product screen)
|
||||
|
||||
1. Ari writes `@maya investigate the refund timeout` as a channel root message.
|
||||
2. Paperclip verifies the Slack signature, creates the durable delivery, deduplicates the event ID, resolves Ari, checks channel reach/authority, and acknowledges within Slack's deadline.
|
||||
3. Maya reacts or posts a short receipt under the root. The root's `thread_ts` becomes the external key and binds exactly one issue assigned to Maya.
|
||||
4. Ari's later thread replies, files, buttons, or modal submissions become turns on that issue. Reactions are deduplicated observational activity only and never create a comment, wake an agent, or convey authority. A modal-opening callback uses a fast acknowledgement path before durable follow-up because Slack trigger IDs expire quickly.
|
||||
5. Safe output streams or edits inside the thread. Stop/actions resolve through Paperclip permissions. The final publication records its provider message ID; failures become a retryable Paperclip publication, never leaked internal traces.
|
||||
|
||||
## 3. GitHub
|
||||
|
||||
The [pinned Chat SDK GitHub adapter](https://github.com/vercel/chat/blob/51322dde8f4aafd8a7fc7a20cbfd7ae45cafaa5c/packages/adapter-github/README.md) treats issues and PRs as threads and supports issue/PR/review-comment webhooks. GitHub recommends selecting the [minimum GitHub App permissions](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app) and lets installers restrict an app to selected repositories.
|
||||
|
||||
### Setup and external handoff — screen 16
|
||||
|
||||
The required customer-owned-App path has three screens:
|
||||
|
||||
1. **Create or connect GitHub App:** copy Paperclip's webhook URL and one-time generated webhook secret into a customer-owned GitHub App, grant the exact required permissions/events, then enter the App ID and private-key PEM write-only in Paperclip.
|
||||
2. **Choose repositories:** use GitHub's installation UI to choose the account/organization and all or selected repositories, then install the customer-owned App.
|
||||
3. **Try Maya:** mention the App in an installed issue or pull request and add another comment to continue the same Paperclip task.
|
||||
|
||||
New and existing GitHub Apps use the same manual credential path. Paperclip generates and stores the webhook secret, shows it once for copying to GitHub, and never returns it from normal endpoint reads. The operator supplies only App ID and the PEM file after configuring GitHub. A GitHub App Manifest create-and-return exchange is an optional future convenience and cannot gate release. A PAT is absent from the product setup flow. The chat-purpose App never requests Contents, Actions, Administration, or other code/tool permissions.
|
||||
|
||||
### Post-connect settings — screen 17
|
||||
|
||||
- **Repositories:** list repositories available to the GitHub App installation and let a Paperclip admin enable or disable each one. A repository added to the installation appears disabled until enabled here.
|
||||
- **Manage GitHub installation:** opens GitHub's repository-selection UI; it changes provider availability, not Paperclip enablement.
|
||||
- **Fixed behavior:** direct mention binds an issue, PR conversation, or inline review thread. Those three provider objects use distinct external keys. Label activation and trusted-author automation are omitted from the first release.
|
||||
- **Activity repairs:** suspended installations, invalid private keys, webhook failures, or permission drift expose contextual repair actions only when detected.
|
||||
|
||||
GitHub host, App identity, private keys, surfaces, activation policy, delivery, reactions, GFM output, edits, attachments, and Paperclip-link fallbacks do not appear in Settings. GitHub Discussions remain outside the launch promise until implemented and tested.
|
||||
|
||||
The current GitHub chat adapter is text-only for inbound content. A URL written in an issue, pull-request, or review comment remains ordinary comment text; Paperclip does not fetch it, ingest it as a file, or treat it as an attachment. Outbound work products use authenticated Paperclip links because GitHub chat has no native file-upload surface.
|
||||
|
||||
### Runtime interaction model (not a product screen)
|
||||
|
||||
1. Ari mentions `@maya` in an allowed issue comment, PR conversation comment, or inline review thread.
|
||||
2. Paperclip validates `X-Hub-Signature-256`, claims the delivery ID, resolves the GitHub principal and installation/repository, applies reach and permission checks, and ignores the app's own comments.
|
||||
3. The existing GitHub object/thread binds once to a Paperclip issue. The PR conversation and an inline review-comment thread can therefore map to separate Paperclip issues even inside the same PR.
|
||||
4. Maya adds a receipt reaction and posts one GFM progress comment. Updates edit that comment at a coarse cadence; the final response replaces or completes it.
|
||||
5. Supported questions and confirmations become explanatory GFM plus a Paperclip URL because GitHub has no native governed-action surface. Richer governance interactions remain Paperclip-only. A request to inspect or modify code runs only if the separately granted GitHub tool connection permits it.
|
||||
|
||||
## 4. Microsoft Teams
|
||||
|
||||
The [pinned Chat SDK Teams adapter](https://github.com/vercel/chat/blob/51322dde8f4aafd8a7fc7a20cbfd7ae45cafaa5c/packages/adapter-teams/README.md) supports personal, team, and group-chat conversations, Adaptive Cards, targeted messages, and request-scoped DM streaming. Paperclip's production webhook path defers work into its durable queue, so that request-scoped streamer is no longer available when output publishes: the shipped endpoint therefore advertises `nativeStreaming: false` and uses bounded post/edit behavior on every Teams surface. Personal-chat Bot Framework file-download attachments can be ingested through the adapter's scoped bot or anonymous download contract, subject to Paperclip's allowed-content policy and configured attachment ceiling (10 MB by default). Channel and group-chat files remain provider references unless a separate Microsoft Graph connection grants access. The pinned transport has no production-safe deferred binary-upload contract, so outbound files use authenticated Paperclip task links on every Teams surface; it does not claim a native Teams upload. Microsoft's [Teams app registration quickstart](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-sdk/get-started/quickstart-register) covers the customer-owned app/bot infrastructure, public endpoint, Teams app configuration, and tenant installation policy.
|
||||
|
||||
### Setup and external handoff — screen 19
|
||||
|
||||
The required path is customer-owned and has two Paperclip screens around provider-owned registration:
|
||||
|
||||
1. **Connect Teams app:** copy Paperclip's messaging endpoint; create the single-tenant Entra App, client secret, Azure Bot, and customer-owned Teams app in Microsoft's portals; apply the displayed bot scopes and resource-specific consent entries; then enter Application/Client ID, Directory/Tenant ID, and client secret in Paperclip.
|
||||
2. **Try Maya:** publish or upload the customer-owned Teams app according to tenant policy, install it in the intended scope, start a new channel post, mention Maya, and reply once beneath the post.
|
||||
|
||||
Paperclip does not generate a Teams package or claim to create an install link. It provides an exact Entra, Azure Bot, Teams Developer Portal, and Teams upload field map plus a copyable block of the Paperclip-specific manifest fields. Teams Developer Portal or equivalent Microsoft tooling still owns the complete app metadata, icons, package, publication, approval, and installation. No provisioning helper is shipped or required.
|
||||
|
||||
The basic setup does not request organization-wide Graph directory or chat history access. Public versus sovereign cloud and advanced identity are deployment/tenant concerns surfaced only when a real incompatibility occurs. Installation policy and Microsoft admin consent stay inside Microsoft's install experience.
|
||||
|
||||
### Post-connect settings — screen 20
|
||||
|
||||
- **Channels:** list channels in teams where Maya is installed and let a Paperclip admin enable or disable each one. Tenant and bot identity cannot change; the tenant appears only as channel context. A later Teams installation appears disabled until enabled here.
|
||||
- **Add Maya to another team:** opens provider instructions; it changes Teams availability, not Paperclip enablement. Channels in the newly installed team then appear disabled in Paperclip.
|
||||
- **Allow direct messages:** one on/off toggle.
|
||||
- **Allow group chats:** a separate on/off toggle, off by default.
|
||||
- **Fixed behavior:** a root channel mention and the replies beneath that post map to one Paperclip task. A personal or group chat has one open task at a time. The next message after completion starts a new task; **New task** starts another explicitly.
|
||||
- **Activity repairs:** app removal, consent revocation, invalid identity, or endpoint failures expose contextual repair actions only when detected.
|
||||
|
||||
RSC, Graph history/directory access, task boundaries, delivery, identity strategy, consent summaries, Adaptive Cards, buttons, task modules, files, reactions, typing, streaming, and buffered/edit behavior do not appear in Settings. Paperclip requests only the minimal provider permission required for the fixed addressed-thread behavior; if Microsoft cannot deliver an unmentioned reply, the conversation asks the person to mention Maya again rather than exposing a policy setting.
|
||||
|
||||
### Runtime interaction model (not a product screen)
|
||||
|
||||
1. **Channel:** Ari mentions Maya in a new channel post. That root post and its replies are the native thread and bind one Paperclip issue.
|
||||
2. **DM/group chat:** the stable Teams conversation has one open Paperclip task. After it completes, the next message starts another; **New task** starts another explicitly without pretending there is a channel-style thread.
|
||||
3. Paperclip verifies the bot activity, tenant, resource, and member; resolves the external principal; checks current permission; then durably appends/wakes the issue.
|
||||
4. DM, channel, and group output use bounded post/edit behavior and may use Adaptive Cards and task modules. File references require a separate Microsoft Graph connection for ingestion; outbound files otherwise use a safe authenticated Paperclip-link fallback on every Teams surface.
|
||||
5. Without RSC, unmentioned ambient channel/chat messages are ignored or not delivered. A denied action uses a targeted response when available, otherwise DM or text plus a Paperclip link.
|
||||
|
||||
The exact delivery of unmentioned replies in a bound Teams channel thread must be proven against the implementation SDK/manifest. If the bot cannot receive them without RSC, the UI must say **Mention Maya on each reply** or request resource-specific consent; it must not imply a subscription it does not have.
|
||||
|
||||
## 5. Discord
|
||||
|
||||
Discord uses the pinned Chat SDK Discord adapter through a long-lived Gateway client. It does not receive a public webhook and does not require an interactions public key because the current product has no Discord slash-command or modal surface.
|
||||
|
||||
### Setup and external handoff
|
||||
|
||||
The complete first-release path is a customer-owned bot:
|
||||
|
||||
1. **Connect Discord bot:** create a dedicated application in Discord Developer Portal, copy its Application ID, enable Message Content Intent, enter the authorized Server ID, and paste the bot token write-only into Paperclip.
|
||||
2. **Install in Discord:** inspect and open Paperclip's server-pinned OAuth URL. It requests only the `bot` scope and permission integer `309237763136`; Administrator, Manage Server, and `applications.commands` are absent.
|
||||
3. **Try Maya:** enable one discovered text channel, post a root `@Maya` message, and reply once inside the public Discord thread Paperclip creates.
|
||||
|
||||
Paperclip verifies that the token belongs to the declared Application ID, the privileged intent is enabled, the bot is installed in the declared server, and usable text channels have the required effective permissions. Application ID is globally unique across active endpoints, including endpoints that name different servers, because one native bot identity cannot represent multiple immutable Paperclip agents.
|
||||
|
||||
### Post-connect settings
|
||||
|
||||
- **Channels:** list text channels visible to the installed bot and let a Paperclip administrator enable a narrower subset. Newly visible channels remain disabled.
|
||||
- **Allow direct messages:** one on/off toggle, off by default. Guild threads and DM task generations never share a binding.
|
||||
- **Fixed behavior:** a root mention creates one public Discord thread and one Paperclip task; eligible replies continue inside it without another mention. A fresh unmentioned root is ignored.
|
||||
- **Activity repairs:** token rotation, lost server membership, missing Message Content Intent, missing effective channel permissions, Gateway retries, and rate-limit failures appear as contextual diagnostics rather than settings.
|
||||
|
||||
There are no endpoint toggles for reactions, post/edit behavior, embeds, buttons, files, lifecycle edits/deletes, reconnect, or retry timing.
|
||||
|
||||
### Runtime interaction model and qualification boundary
|
||||
|
||||
Discord messages, reactions, interactions, edits, deletes, and partial reaction hydration enter through the Gateway and the same durable delivery/outbox boundary as webhook providers. Safe output uses bounded post/edit behavior; embeds and supported buttons are automatic; file downloads are bounded to reviewed Discord CDN hosts; numeric user IDs are the identity key; callbacks reauthorize against current Paperclip state. Gateway reconnect and provider `retry_after` timing are automatic.
|
||||
|
||||
Paperclip completes endpoint, resource, principal, and root-message preflight before any provider-thread side effect. A denied root creates no Discord thread, task, acknowledgement, reply, or run. For an allowed root, Paperclip durably persists a provisional receipt before asking Discord to create the thread; recovery then creates or reuses that thread idempotently and treats Discord error `160004` as existing-thread reconciliation rather than failure. Bounded provider calls and fail-fast compatibility checks keep SDK drift and stalled REST operations visible. These guarantees have deterministic and fresh-database evidence, but files, interactions, Gateway recovery, rate limits, and the root-activation fault paths still require real-provider qualification before Discord can be called stable.
|
||||
|
||||
## 6. Telegram
|
||||
|
||||
The [pinned Chat SDK Telegram adapter](https://github.com/vercel/chat/blob/51322dde8f4aafd8a7fc7a20cbfd7ae45cafaa5c/packages/adapter-telegram/README.md) supports verified webhooks or polling, files/media, inline buttons, reactions, DMs, throttled post/edit streaming, and opt-in private-chat draft previews. Telegram documents the mutually exclusive [`setWebhook` and `getUpdates`](https://core.telegram.org/bots/api) delivery modes and how [privacy mode](https://core.telegram.org/bots/faq) limits group updates.
|
||||
|
||||
### Setup and external handoff — screen 22
|
||||
|
||||
The normal path has two screens:
|
||||
|
||||
1. **Create Maya:** open BotFather, send `/newbot`, enter Maya's name, choose an available username ending in `bot`, and paste the returned token into Paperclip.
|
||||
2. **Try Maya:** open the new bot's private chat, tap **Start**, and send one test message.
|
||||
|
||||
Telegram has no bot-installation OAuth callback, so the token is the single irreducible credential field. Private chat is the shortest working proof. Group and forum installation moves to post-connect configuration instead of lengthening first setup.
|
||||
|
||||
Public/relay production uses a verified webhook chosen by the deployment; local development may use polling. These mutually exclusive modes are instance behavior, not endpoint setup. A leaked token is rotated at BotFather and the Paperclip secret reference is replaced.
|
||||
|
||||
### Post-connect settings — screen 23
|
||||
|
||||
- **Chats and topics:** list discovered destinations where the bot is present and let a Paperclip admin enable or disable each one. A later Telegram chat/topic discovery appears disabled until enabled here.
|
||||
- **Add Maya to another Telegram chat:** opens provider instructions; it changes Telegram membership, not Paperclip enablement.
|
||||
- **Allow direct messages:** one on/off toggle.
|
||||
- **Fixed behavior:** a DM or ordinary group has one open task at a time; after completion, the next addressed message starts another. `/new` or **New task** starts another explicitly. A forum `message_thread_id` maps one topic to one task. Privacy-on unrelated group traffic is ignored.
|
||||
- **Activity repairs:** invalid token, lost membership, webhook failures, or flood-control problems expose contextual repair actions only when detected.
|
||||
|
||||
Allowed-user lists belong to Access. Task boundaries, BotFather privacy, delivery, relay/polling, token rotation, typing/reactions, post-edit output, private-chat drafts, inline buttons, Markdown, files/media, and safe fallbacks do not appear in Settings.
|
||||
|
||||
### Runtime interaction model (not a product screen)
|
||||
|
||||
1. **DM:** Ari's first message creates the active issue. An inline **New task** button or `/new` intentionally starts a different issue; ordinary replies continue the active one.
|
||||
2. **Ordinary group:** `@maya` creates the active binding. Ari must reply to Maya or mention her for later turns. Privacy-on unrelated traffic is not delivered/processed.
|
||||
3. **Forum group:** the topic's `message_thread_id` is the stable external boundary and can bind one issue. Topic creation is only attempted if configured and authorized.
|
||||
4. Paperclip validates the secret header or polling claim, deduplicates `update_id`, checks chat/user scope and authority, persists the turn, then sends typing/reaction and throttled progress.
|
||||
5. Inline callbacks contain a short opaque lookup key, not authority. Paperclip reauthorizes the principal; unsupported or governed actions receive normal text or DM plus an authenticated Paperclip link.
|
||||
|
||||
## 7. Wireframe annotations
|
||||
|
||||
The numbered red dashed marks in the archived images are review annotations only, not proposed UI. The historical v8 viewer contains 14 minimum setup phases plus four provider management tabs; it contains no interaction-walkthrough pages. Annotation and button-consequence explanations remain in `2026-09-04-chat-adapters-ui-surfaces-v8.md`; its historical setup source data lives in `setup-wireframe-data-v6.mjs` and its historical management source data in `management-wireframe-data-v8.mjs`. The five-provider implementation addenda and live browser runbook are the current product and acceptance sources.
|
||||
|
||||
## 8. Implementation acceptance points exposed by the wires
|
||||
|
||||
- Provider setup has a persistent step rail and can be paused when external admin action is required, then resumed without creating a second endpoint.
|
||||
- The selected agent cannot change. Connecting another agent always creates another endpoint.
|
||||
- Setup page bodies contain only required operator actions and inputs. The completed agent step, automatic Paperclip work, capability lists, and successful checks are not repeated as content.
|
||||
- Authenticated provider handoffs keep credentials invisible. Manual/customer-owned paths expose only irreducible secrets and store them write-only through Paperclip secret references.
|
||||
- Delivery transport is selected by deployment and reported as health; direct/relay/Socket/polling are not connector-wizard choices.
|
||||
- A real provider message completes setup and enables that explicitly exercised destination. Detailed identity, delivery, permission, and capability health appears only when a setup error needs remediation or later in Activity.
|
||||
- There is no read-only Overview tab. Activity reports health and degradation; Settings contains only provider-available destination enablement and direct/group-chat reach toggles.
|
||||
- Access contains only the unlinked-participation decision and explicit identity links. The internal sponsoring principal and fixed authority calculation are not normal settings.
|
||||
- Conversations is a read-only list with provider/task links and row state. It has no manual detach or boundary-management controls.
|
||||
- Basic operation uses the smallest viable provider permission set. RSC, Graph directory/history, Slack Agent Sessions, Telegram topic administration, and GitHub code access are separate upgrades.
|
||||
- Every native conversation representation maps to a clear Paperclip issue boundary and gives the user an explicit way to start a new issue on linear-chat platforms.
|
||||
- Self-message suppression, provider redelivery deduplication, uninstall/revocation, permission drift, rate limits, and provider health appear in Activity even when absent from the happy-path setup.
|
||||
- Desktop/mobile wires preserve 48px mobile targets and the established Paperclip connector shell.
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
# Paperclip Chat Adapters — Setup Audit v5
|
||||
|
||||
Status: historical snapshot; current setup specification is [`2026-09-04-chat-adapters-minimum-setup-v6.md`](./2026-09-04-chat-adapters-minimum-setup-v6.md)
|
||||
Date: 2026-09-04
|
||||
Paperclip base: `7b094724e65c04949706df638d497afb02c84b62`
|
||||
Historical review viewer: [Git archive](./wireframes-archive.md).
|
||||
Archived setup wireframes: [v5 SVG snapshot](https://github.com/paperclipai/paperclip/tree/1c4a45f0ef7d627aa98e4f3ae3116d4507386d1a/doc/plans/chat-adapters/wireframes-v5) ([archive and regeneration notes](./wireframes-archive.md))
|
||||
|
||||
## Decision
|
||||
|
||||
Connector setup asks only for decisions or values that Paperclip cannot safely infer, provision, receive from a provider callback, or inherit from the instance deployment.
|
||||
|
||||
- The selected agent is displayed as **Locked** throughout setup. A bot identity represents one agent for the lifetime of the connection. Connecting another agent creates another connection.
|
||||
- Every provider uses a persistent step rail with completed, current, and remaining phases. A provider redirect may leave Paperclip, but the draft and current phase remain resumable.
|
||||
- Provider-owned approval, organization/workspace choice, repository selection, tenant policy, app installation, and native bot naming remain in the provider's UI.
|
||||
- Paperclip fixes required events, permissions, callback URLs, command declarations, and maximum safe interaction capabilities. They are not setup options.
|
||||
- Paperclip selects delivery from instance reachability. Direct callback, relay, Socket Mode, and polling do not appear as endpoint preferences.
|
||||
- Credentials obtained by an authenticated provider handoff go directly to Paperclip's secret store. They are not displayed or copied through the UI.
|
||||
- A final live mention or message is encouraged because it proves the real installation, delivery, identity, and conversation boundary. It may be skipped so setup does not block on another person or provider administrator.
|
||||
|
||||
## Shared row-by-row disposition
|
||||
|
||||
| Previous row or choice | v5 disposition | Reason |
|
||||
| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Change agent | Remove; show the assigned agent and **Locked** | Changing it would make an established provider bot identity and historical task bindings ambiguous. Create a new connection for another agent. |
|
||||
| Bot name/avatar configuration | Show a read-only preview or provider-owned result | Provider naming and uniqueness rules belong in the provider handoff. Paperclip may propose the agent name and avatar. |
|
||||
| Direct webhook | Remove as a choice | It is the automatic path when the instance has a verified public callback. |
|
||||
| Private Paperclip / relay | Remove as a choice | A private instance uses its configured outbound relay automatically. Relay enrollment and keys belong to instance administration, not to each endpoint. |
|
||||
| Slack Socket Mode | Remove from endpoint setup | It requires an app-level token and persistent outbound listener and has distribution constraints. It is an instance-admin development/on-premises escape hatch only. |
|
||||
| Telegram polling | Remove from endpoint setup | Polling and webhook delivery are mutually exclusive. Paperclip may use polling for a local developer instance, never as a normal endpoint preference. |
|
||||
| Feature switches for reactions, streaming, cards, actions, modals, commands, files, edits, or DMs | Remove | Paperclip always uses the maximum safe feature supported by the adapter, installation, conversation, and current Paperclip authorization. |
|
||||
| Event/scopes checklist | Generate and verify; do not expose toggles | Chat connectors need a known least-privilege contract. Missing permissions become a repair state, not an optional configuration. |
|
||||
| Credentials returned by OAuth or manifest callback | Hide completely | Paperclip can store them directly without asking the operator to handle a secret. |
|
||||
| Customer-owned credentials with no callback | Keep only the irreducible values; submit write-only | Paperclip cannot authenticate without them. The connector shows secret references and rotation state after setup, never the stored values. |
|
||||
| Provider resource choice | Keep in the provider handoff | Workspace, organization, repository, tenant, team, channel, group, or chat membership is governed by provider policy. Paperclip may narrow the returned scope later. |
|
||||
| Send test | Replace synthetic tests with a real native mention/message | A real event proves signature/authentication, installation scope, native identity, routing, and task binding together. |
|
||||
|
||||
## Delivery model
|
||||
|
||||
| Deployment condition | What Paperclip does | What the endpoint wizard shows |
|
||||
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| Paperclip Cloud or publicly reachable self-hosted instance | Registers the endpoint's unguessable verified HTTPS callback directly with the provider. | **Automatic** during setup; read-only delivery health after setup. |
|
||||
| Private self-hosted instance with Paperclip relay configured | The instance maintains an authenticated outbound relay connection; the relay accepts the provider callback and forwards the verified envelope. | **Automatic** during setup; relay health at instance administration and read-only endpoint diagnostics. |
|
||||
| Local/developer instance without a public callback or relay | May run a provider-specific escape hatch such as Slack Socket Mode or Telegram polling. | Nothing in normal endpoint setup. The developer enables it once at instance level. |
|
||||
|
||||
The direct callback is preferred because it has the fewest moving parts. A private instance cannot receive that callback from Slack, GitHub, Teams, or Telegram; that is the reason a relay exists. Slack Socket Mode establishes an outbound WebSocket using an app-level token, so it avoids a public Request URL but requires a continuously running listener. It is not a competing UX choice. Telegram polling is the analogous local-development fallback and cannot run while a webhook is registered.
|
||||
|
||||
## Credentials retained after simplification
|
||||
|
||||
| Provider path | Values typed or uploaded by the operator | Why they remain |
|
||||
| ------------------------------------ | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Slack — customer-owned app | Bot token and signing secret | Slack's app-from-manifest handoff preconfigures the App but does not return these two customer-owned values to Paperclip. No webhook URL, app token, or delivery choice is requested. |
|
||||
| GitHub — customer-owned App | App ID and private-key PEM; GitHub Enterprise Server host only when applicable | Paperclip generates, stores, and reveals the webhook secret once for copying to GitHub. It then authenticates and verifies the App callback, events, and permissions without asking the operator to paste the secret back. |
|
||||
| Microsoft Teams — customer-owned bot | Application/client ID, tenant ID, client secret | These values come from the customer's Entra App and Azure Bot registration. No provisioning helper is shipped or required. Managed identity remains an instance-level advanced deployment path. |
|
||||
| Telegram — BotFather bot | Bot token | Telegram has no OAuth or app-manifest installation callback. BotFather gives the operator the bot password once. |
|
||||
|
||||
All secrets are write-only inputs to Paperclip's existing secret store. Setup and connector detail retain only secret references, redacted suffixes, health, and rotation actions.
|
||||
|
||||
## Slack setup inventory
|
||||
|
||||
### Required customer-owned App path
|
||||
|
||||
| Screen | Phase | Retained action | What happens |
|
||||
| ------ | ---------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 13 | Create Slack app | **Open prefilled Slack setup** | Opens Slack's app-from-manifest URL with identity, callback URLs, scopes, events, interactivity, commands, and files prepared. |
|
||||
| 13 | Connect app | **Save and verify** | Stores the bot token and signing secret write-only, calls Slack identity APIs, and verifies required scopes. |
|
||||
| 41 | Try Maya | **Open Slack** | Opens the installed workspace while Paperclip waits for a signed root mention. A valid mention creates the Slack thread and its one Paperclip task. |
|
||||
| 41 | Try Maya | **Finish without testing** | Activates the endpoint after installation checks and leaves first-event verification visible on Overview. |
|
||||
|
||||
Normal Slack setup has only the unavoidable bot-token and signing-secret inputs. Callback, relay, Socket Mode, app-token, event, scope, and feature choices remain absent.
|
||||
|
||||
### Optional managed install
|
||||
|
||||
An Add to Slack flow can be introduced when Paperclip participates in Slack's managed agent-deployment program. It is a convenience only and is not a first-release dependency.
|
||||
|
||||
Slack's OAuth installation redirects through Slack, and its app manifest can create a preconfigured customer-owned app. Socket Mode remains an instance-level exception because Slack documents it as an outbound WebSocket connection using an app-level token and notes distribution limitations. See [Slack OAuth installation](https://docs.slack.dev/authentication/installing-with-oauth/), [Slack App Manifests](https://docs.slack.dev/app-manifests/configuring-apps-with-app-manifests/), [Slack Socket Mode](https://docs.slack.dev/apis/events-api/using-socket-mode/), and [Add to Slack](https://slack.com/intl/en-ie/blog/news/add-to-slack).
|
||||
|
||||
## GitHub setup inventory
|
||||
|
||||
### Required customer-owned App path
|
||||
|
||||
| Screen | Phase | Retained action | What happens |
|
||||
| ------ | ------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 16 | Create GitHub App | **Generate webhook secret** | Paperclip generates and stores a 32-byte secret and reveals it once for copying into the GitHub App. |
|
||||
| 16 | Connect GitHub App | **Connect and verify** | Accepts the App ID and private-key PEM, authenticates as the App, and verifies the callback, events, and least-privilege permissions. |
|
||||
| 45 | Choose repositories | **Install in GitHub** | GitHub owns account/organization approval and all-vs-selected repository choice, then returns the installation ID. |
|
||||
| 46 | Try Maya | **Open GitHub** | Opens an installed repository while Paperclip waits for a signed mention in an issue, PR conversation, or inline review thread. |
|
||||
| 46 | Try Maya | **Finish without testing** | Activates after App and installation verification; first-delivery status remains on Overview. |
|
||||
|
||||
The required path asks only for the App ID and private-key PEM after Paperclip has generated the webhook secret. Contents, Actions, and Administration permissions are absent because this is a chat connection; a GitHub tool connection is separate.
|
||||
|
||||
### Existing App
|
||||
|
||||
| Screen | Phase | Retained action | What happens |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | --------------- | ------------ |
|
||||
| An existing App uses the same generated-secret, App ID, and private-key path. Regenerating the webhook secret is an explicit rotation and requires updating GitHub before signed deliveries can resume. |
|
||||
|
||||
GitHub's App Manifest exchange remains a possible managed convenience, not a release dependency. GitHub still owns repository installation and scope selection. See [registering a GitHub App from a manifest](https://docs.github.com/en/apps/sharing-github-apps/registering-a-github-app-from-a-manifest) and [installing a GitHub App from a third party](https://docs.github.com/en/apps/using-github-apps/installing-a-github-app-from-a-third-party).
|
||||
|
||||
## Microsoft Teams setup inventory
|
||||
|
||||
Microsoft currently requires more customer-owned infrastructure than the other default paths. v5 does not present multiple authentication or delivery strategies. It chooses a single-tenant client-secret flow for the portable first release and moves managed identity/federation to instance-level advanced deployment.
|
||||
|
||||
| Screen | Phase | Retained action | What happens |
|
||||
| ------ | ------------------ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 19 | Register Teams bot | **Open Microsoft setup** | Guides the operator through a customer-owned single-tenant Entra App and Azure Bot registration using Paperclip's messaging endpoint. |
|
||||
| 19 | Register Teams bot | **Copy messaging endpoint** | Copies the exact public callback to enter in the Azure Bot configuration. |
|
||||
| 48 | Connect identity | **Save and verify** | Stores the client secret write-only, requests a Microsoft bot token, and verifies tenant, application, and messaging endpoint. |
|
||||
| 49 | Install app | **Download Teams package** | Downloads a validated ZIP containing public manifest metadata and icons; it contains no secret. |
|
||||
| 49 | Install app | **Open Teams** | Opens Teams app management for upload/install. Tenant policy decides self-service vs administrator approval. |
|
||||
| 50 | Try Maya | **Open Microsoft Teams** | Opens Teams while Paperclip waits for the first authenticated activity from an installed scope. |
|
||||
| 50 | Try Maya | **Finish without testing** | Activates after identity and package checks; installation delivery remains pending on Overview until a real activity arrives. |
|
||||
|
||||
Paperclip generates the endpoint, manifest values, and package. Microsoft owns tenant sign-in, Azure/Entra resource creation, app approval, and installation scope. See [Teams SDK registration quickstart](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-sdk/get-started/quickstart-register), [Teams app authentication](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-sdk/essentials/app-authentication/overview), [Azure configuration](https://learn.microsoft.com/en-us/microsoftteams/platform/teams-sdk/teams/azure-configuration), and [publishing/installing Teams apps](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/publish).
|
||||
|
||||
No Teams provisioning helper is shipped or required; the customer-owned path is complete.
|
||||
|
||||
## Telegram setup inventory
|
||||
|
||||
| Screen | Phase | Retained action | What happens |
|
||||
| ------ | ------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 22 | Create Telegram bot | **Connect bot** | Stores the BotFather token write-only, calls `getMe`, fixes the immutable native bot identity, registers commands, and configures deployment-selected delivery. |
|
||||
| 22 | Create Telegram bot | **Open BotFather** | Opens the provider flow where the operator runs `/newbot`, chooses an available username, and receives the token. |
|
||||
| 51 | Add to chats | **Open Maya in Telegram** | Opens the bot profile so the operator can start a DM or add it to a group/forum under Telegram membership policy. |
|
||||
| 52 | Try Maya | **Open Telegram** | Opens Telegram while Paperclip waits for a real update from the intended DM, group, or forum topic. |
|
||||
| 52 | Try Maya | **Finish without testing** | Activates after bot identity checks and leaves chat-membership delivery pending on Overview. |
|
||||
|
||||
Paperclip does not ask for chat IDs up front. It learns stable chat, forum-topic, and participant identifiers from authenticated updates and lets an operator approve them afterward. BotFather's token is unavoidable because Telegram has no OAuth-style bot installation callback. Webhook or local polling selection is automatic. See [Telegram's BotFather tutorial](https://core.telegram.org/bots/tutorial) and [Telegram Bot API webhook/polling contract](https://core.telegram.org/bots/api).
|
||||
|
||||
## Purpose choice for dual-surface connectors
|
||||
|
||||
Screen 02 is registry-driven, not GitHub-specific. Any connector declaring both `chat` and `tool` methods asks one question:
|
||||
|
||||
- **Chat with an agent** enters the chat wizard, selects one immutable agent, and creates a native conversation endpoint.
|
||||
- **Use this connection as an agent tool** enters Paperclip's existing connection credential and human/agent-access flow.
|
||||
|
||||
Connectors declaring only one method skip the choice entirely.
|
||||
|
||||
## Setup state and recovery
|
||||
|
||||
Each phase persists a draft with the immutable agent, provider handoff nonce, completed checks, expiration, and safe remediation state. Provider returns are idempotent. Refreshing or returning after administrator approval resumes the current phase. Revoked, expired, wrong-company, permission-denied, and provider-error returns explain the corrective action without revealing credentials. Abandoning setup deletes only the unactivated draft; it does not delete a provider resource without a separate explicit action.
|
||||
|
||||
## What remains configurable after activation
|
||||
|
||||
- Resource reach within the provider installation: channels, repositories, teams/channels, Telegram chats/topics.
|
||||
- Identity links, endpoint sponsor, and restricted external-person access.
|
||||
- Conversation activation and task-boundary behavior where the provider genuinely offers alternatives.
|
||||
- Explicit trusted automation or broader-consent grants, default off.
|
||||
- Secret rotation only for customer-owned credential paths.
|
||||
- Pause, reconnect/repair, test, and remove lifecycle actions.
|
||||
|
||||
Delivery transport and response capabilities remain status, not preferences.
|
||||
|
|
@ -0,0 +1,597 @@
|
|||
# Paperclip Chat Adapters UI Surfaces — v6
|
||||
|
||||
> Historical revision. The current review is [`2026-09-04-chat-adapters-ui-surfaces-v8.md`](./2026-09-04-chat-adapters-ui-surfaces-v8.md). Managed-install and helper-first concepts below are not shipped requirements.
|
||||
|
||||
Date: 2026-09-04
|
||||
Paperclip base: `7b094724e65c04949706df638d497afb02c84b62`
|
||||
Historical review viewer: [Git archive](./wireframes-archive.md).
|
||||
Archived wireframes: [v6 SVG snapshot](https://github.com/paperclipai/paperclip/tree/1c4a45f0ef7d627aa98e4f3ae3116d4507386d1a/doc/plans/chat-adapters/wireframes-v6) ([archive and regeneration notes](./wireframes-archive.md))
|
||||
Minimum-setup specification: [`2026-09-04-chat-adapters-minimum-setup-v6.md`](./2026-09-04-chat-adapters-minimum-setup-v6.md)
|
||||
|
||||
## Relevance rule
|
||||
|
||||
A setup screen may show only something the operator must click, copy, paste, upload, choose, or perform at the provider during that step. Do not repeat the selected agent, describe automatic Paperclip work, list capabilities, or show successful checks. Errors and unmet prerequisites appear only when they occur.
|
||||
|
||||
## Current setup inventory
|
||||
|
||||
- Slack: Add to Slack and a three-step customer-owned-App fallback converge on one test screen.
|
||||
- GitHub: App Manifest creation, repository installation, and test; existing App is an advanced fallback.
|
||||
- Microsoft Teams: one guided command, one install link, and test; manual Microsoft registration is an advanced fallback.
|
||||
- Telegram: BotFather token and one private-message test.
|
||||
- Capabilities and health remain on Overview and the interaction walkthroughs, never in setup.
|
||||
|
||||
## Inventory
|
||||
|
||||
| ID | Group | Surface | Title | Desktop | Mobile |
|
||||
| --- | --------------- | ------------------------ | -------------------------------------- | --------- | -------- |
|
||||
| 01 | Start | Shared | Connectors | 1280×800 | 375×812 |
|
||||
| 02 | Start | Shared | Choose how to connect | 1280×800 | 375×812 |
|
||||
| 03 | Start | Shared | Which agent do you want to chat with? | 1280×800 | 375×812 |
|
||||
| 13 | Slack | Setup | Add Maya to Slack | 1280×800 | 375×812 |
|
||||
| 42 | Slack | Custom setup | Create and install the Slack app | 1280×800 | 375×1064 |
|
||||
| 43 | Slack | Custom setup | Connect the Slack app | 1280×800 | 375×1176 |
|
||||
| 41 | Slack | Setup | Try Maya in Slack | 1280×800 | 375×944 |
|
||||
| 25 | Slack | Overview | Slack overview | 1280×1472 | 375×1928 |
|
||||
| 14 | Slack | Settings | Slack settings | 1280×1250 | 375×1676 |
|
||||
| 26 | Slack | Access | Slack access | 1280×1256 | 375×1592 |
|
||||
| 27 | Slack | Conversations | Slack conversations | 1280×1160 | 375×1600 |
|
||||
| 28 | Slack | Activity | Slack activity | 1280×1200 | 375×1640 |
|
||||
| 15 | Slack | Conversation walkthrough | How Slack conversations work | 1280×960 | 375×1320 |
|
||||
| 16 | GitHub | Setup | Create Maya in GitHub | 1280×800 | 375×952 |
|
||||
| 45 | GitHub | Setup | Choose GitHub repositories | 1280×800 | 375×1000 |
|
||||
| 46 | GitHub | Setup | Try Maya in GitHub | 1280×800 | 375×1000 |
|
||||
| 47 | GitHub | Custom setup | Connect an existing GitHub App | 1280×960 | 375×1392 |
|
||||
| 29 | GitHub | Overview | GitHub overview | 1280×1472 | 375×1928 |
|
||||
| 17 | GitHub | Settings | GitHub settings | 1280×1178 | 375×1564 |
|
||||
| 30 | GitHub | Access | GitHub access | 1280×1256 | 375×1592 |
|
||||
| 31 | GitHub | Conversations | GitHub conversations | 1280×1160 | 375×1600 |
|
||||
| 32 | GitHub | Activity | GitHub activity | 1280×1200 | 375×1640 |
|
||||
| 18 | GitHub | Conversation walkthrough | How GitHub conversations work | 1280×960 | 375×1320 |
|
||||
| 19 | Microsoft Teams | Setup | Create Maya for Microsoft Teams | 1280×800 | 375×1080 |
|
||||
| 49 | Microsoft Teams | Setup | Install Maya in Microsoft Teams | 1280×800 | 375×888 |
|
||||
| 50 | Microsoft Teams | Setup | Try Maya in Microsoft Teams | 1280×800 | 375×1000 |
|
||||
| 48 | Microsoft Teams | Custom setup | Set up Microsoft manually | 1280×1064 | 375×1496 |
|
||||
| 33 | Microsoft Teams | Overview | Microsoft Teams overview | 1280×1472 | 375×1928 |
|
||||
| 20 | Microsoft Teams | Settings | Microsoft Teams settings | 1280×1322 | 375×1788 |
|
||||
| 34 | Microsoft Teams | Access | Microsoft Teams access | 1280×1256 | 375×1592 |
|
||||
| 35 | Microsoft Teams | Conversations | Microsoft Teams conversations | 1280×1160 | 375×1600 |
|
||||
| 36 | Microsoft Teams | Activity | Microsoft Teams activity | 1280×1200 | 375×1640 |
|
||||
| 21 | Microsoft Teams | Conversation walkthrough | How Microsoft Teams conversations work | 1280×960 | 375×1320 |
|
||||
| 22 | Telegram | Setup | Create Maya in Telegram | 1280×800 | 375×1128 |
|
||||
| 51 | Telegram | Setup | Try Maya in Telegram | 1280×800 | 375×832 |
|
||||
| 37 | Telegram | Overview | Telegram overview | 1280×1472 | 375×1928 |
|
||||
| 23 | Telegram | Settings | Telegram settings | 1280×1322 | 375×1788 |
|
||||
| 38 | Telegram | Access | Telegram access | 1280×1256 | 375×1592 |
|
||||
| 39 | Telegram | Conversations | Telegram conversations | 1280×1160 | 375×1600 |
|
||||
| 40 | Telegram | Activity | Telegram activity | 1280×1200 | 375×1640 |
|
||||
| 24 | Telegram | Conversation walkthrough | How Telegram conversations work | 1280×960 | 375×1320 |
|
||||
| 11 | Paperclip | Task | Externally bound task | 1280×800 | 375×812 |
|
||||
| 12 | Paperclip | Agent | Agent Channels | 1280×800 | 375×812 |
|
||||
|
||||
## Annotation and action notes
|
||||
|
||||
### 01 · Connectors
|
||||
|
||||
Purpose: Connect tools and places where people talk to agents.
|
||||
|
||||
1. The existing Apps catalog remains the entry point.
|
||||
2. Filters separate chat and tool methods.
|
||||
3. Each connector row has one Connect action.
|
||||
4. Connection state remains visible in the catalog.
|
||||
|
||||
Rationale: The current Connectors surface remains canonical.
|
||||
|
||||
### 02 · Choose how to connect
|
||||
|
||||
Purpose: Shown for every connector that supports both chat and tool methods.
|
||||
|
||||
1. The existing connection wizard shell and selected provider are reused.
|
||||
2. Chat with an agent is the incoming-conversation path.
|
||||
3. Use this connection as an agent tool is the outbound tool/credential path.
|
||||
4. Single-purpose providers skip the choice.
|
||||
|
||||
Rationale: The registry drives the same direction choice for every dual-surface connector.
|
||||
|
||||
### 03 · Which agent do you want to chat with?
|
||||
|
||||
Purpose: Choose the one agent represented by this connection.
|
||||
|
||||
1. The existing agent selector is reused.
|
||||
2. Only active agents can be selected.
|
||||
3. One selection is required.
|
||||
4. Continue begins provider setup.
|
||||
|
||||
Rationale: This is the only shared Paperclip-specific setup decision.
|
||||
|
||||
### 13 · Add Maya to Slack
|
||||
|
||||
Purpose: Install Maya in your Slack workspace.
|
||||
|
||||
1. The step rail is the only repeated setup context; the selected agent is not restated in the page body.
|
||||
2. The page contains only the installation action and the necessary customer-owned-App fallback.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Add Maya to Slack:** Opens Slack's Add to Slack flow. The operator chooses a workspace and approves the installation; Slack then returns to the Try Maya step.
|
||||
- **Set up a custom Slack app:** Opens the customer-owned Slack App instructions for self-hosted deployments or organizations that cannot use Add to Slack.
|
||||
|
||||
Rationale: Nothing else on this page requires operator attention.
|
||||
|
||||
### 42 · Create and install the Slack app
|
||||
|
||||
Purpose: Paperclip prepared a Slack App Manifest for Maya.
|
||||
|
||||
1. Every line is an action the operator must complete in Slack.
|
||||
2. The manifest removes manual scope, event, callback, command, and interactivity configuration.
|
||||
3. The page advances only after the operator confirms the app was installed.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open Slack app setup:** Opens Slack's official app-from-manifest URL with Paperclip's generated manifest encoded in the link.
|
||||
- **Continue after installing:** Advances to the two credential fields after the operator has installed the new app in Slack.
|
||||
|
||||
Rationale: The custom path gives exact provider instructions without exposing Paperclip's automatic configuration.
|
||||
|
||||
### 43 · Connect the Slack app
|
||||
|
||||
Purpose: Copy two values from the Slack app settings.
|
||||
|
||||
1. The only help text tells the operator exactly where to find each required value.
|
||||
2. Only the two unavoidable Slack credentials are requested.
|
||||
3. Connecting verifies the values instead of showing a separate verification report.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Connect Slack app:** Stores both values write-only and verifies the Slack bot identity and required scopes before continuing.
|
||||
- **Back:** Returns to the Slack creation instructions without saving partially entered values.
|
||||
|
||||
Rationale: A customer-owned Slack App cannot return these values to Paperclip, so both fields are necessary.
|
||||
|
||||
### 41 · Try Maya in Slack
|
||||
|
||||
Purpose: Start one task and reply to it once.
|
||||
|
||||
1. The body is only the three actions needed to test the real Slack interaction.
|
||||
2. The instructions teach the root-mention-to-thread Paperclip task boundary.
|
||||
3. There is one action: open Slack and perform the test.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open Slack:** Opens the installed workspace while Paperclip waits for the root mention and thread reply to complete setup.
|
||||
|
||||
Rationale: Installation health and automatic verification do not belong on an instruction screen.
|
||||
|
||||
### 25 · Slack overview
|
||||
|
||||
Purpose: Identity, health, capabilities, and lifecycle.
|
||||
|
||||
1. The endpoint keeps one Paperclip agent and one provider-native bot identity together.
|
||||
2. Installation and delivery health are summarized before any configuration detail.
|
||||
3. Every safe capability available to this provider is included automatically; this is status, not a set of switches.
|
||||
4. Test, pause, reconnect, and remove remain ordinary connector lifecycle actions.
|
||||
|
||||
Rationale: Overview remains provider-specific and outside onboarding.
|
||||
|
||||
### 14 · Slack settings
|
||||
|
||||
Purpose: Scope, task boundaries, and necessary provider operations.
|
||||
|
||||
1. Reach is an operator choice and is always bounded by the Slack installation and actual bot membership.
|
||||
2. Root mention, native thread creation, subscribed replies, and DM task boundaries are explicit.
|
||||
3. Delivery is read-only status; only credential rotation and installation repair require operator action here. Slack capabilities are reported on Overview and demonstrated in the walkthrough, never configured here.
|
||||
|
||||
Rationale: Settings remains provider-specific and outside onboarding.
|
||||
|
||||
### 26 · Slack access
|
||||
|
||||
Purpose: Identity links, sponsored guests, and effective authority.
|
||||
|
||||
1. The endpoint sponsor supplies the maximum authority available to unlinked external people.
|
||||
2. Linked provider identities act as their current Paperclip users and retain ordinary permission checks.
|
||||
3. Unlinked people use the restricted sponsored-guest profile and cannot perform governance actions.
|
||||
4. Provider identity and scope details make effective authority explainable and auditable.
|
||||
|
||||
Rationale: Access remains provider-specific and outside onboarding.
|
||||
|
||||
### 27 · Slack conversations
|
||||
|
||||
Purpose: Native conversation-to-Paperclip task bindings.
|
||||
|
||||
1. Each row names the provider-native conversation boundary and its single Paperclip issue.
|
||||
2. Participants, assigned agent, state, and last activity make live bindings scannable.
|
||||
3. Open in provider and Open task take an operator to either side of the binding.
|
||||
4. Detach preserves history and publication records; a later activation creates or claims a new binding.
|
||||
|
||||
Rationale: Conversations remains provider-specific and outside onboarding.
|
||||
|
||||
### 28 · Slack activity
|
||||
|
||||
Purpose: Provider health, deliveries, publications, and retries.
|
||||
|
||||
1. Provider, credential, callback, and deployment-selected delivery health are summarized in one operational section.
|
||||
2. Inbound deliveries, callbacks, and outbound publications share a durable chronological ledger.
|
||||
3. Operators can inspect redacted errors and replay only safe, authorized failed deliveries.
|
||||
4. Rate limits, permission drift, uninstall or revocation, and provider-specific diagnostics stay visible.
|
||||
|
||||
Rationale: Activity remains provider-specific and outside onboarding.
|
||||
|
||||
### 15 · How Slack conversations work
|
||||
|
||||
Purpose: The provider-native interaction and fallback model.
|
||||
|
||||
1. Ari starts in a Slack channel with a root @maya mention; unrelated root messages do not start work.
|
||||
2. Maya acknowledges inside a Slack thread, making the thread—not the channel—the visible conversation boundary.
|
||||
3. Paperclip creates exactly one assigned issue and shows its Slack source, external participant, and publication state.
|
||||
4. Ari continues by replying in the same thread without another mention; files and actions remain in that context.
|
||||
5. Maya's safe progress and final answer publish in the thread; failures offer retry or a Paperclip link.
|
||||
|
||||
Rationale: Capabilities are demonstrated here, not configured during setup.
|
||||
|
||||
### 16 · Create Maya in GitHub
|
||||
|
||||
Purpose: Create a dedicated GitHub App from Paperclip's prepared manifest.
|
||||
|
||||
1. Only the two choices GitHub presents during App creation are described.
|
||||
2. The normal action uses the GitHub App Manifest handoff; credentials never pass through the operator.
|
||||
3. The existing-App branch remains available without cluttering the default path.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Create in GitHub:** Posts Paperclip's App Manifest to GitHub. GitHub returns to Paperclip after creation, and Paperclip stores the returned App credentials.
|
||||
- **Use an existing GitHub App:** Opens the advanced path for an App the organization already owns.
|
||||
|
||||
Rationale: The manifest already fixes permissions, events, and webhook configuration.
|
||||
|
||||
### 45 · Choose GitHub repositories
|
||||
|
||||
Purpose: Install Maya where people should be able to mention it.
|
||||
|
||||
1. The screen contains only GitHub's installation decisions.
|
||||
2. Repository scope stays in GitHub's native approval UI.
|
||||
3. One button begins the complete provider-owned installation step.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Install in GitHub:** Opens GitHub's App installation page and returns the installation and selected repository IDs to Paperclip.
|
||||
|
||||
Rationale: There is no Paperclip form to duplicate GitHub's repository picker.
|
||||
|
||||
### 46 · Try Maya in GitHub
|
||||
|
||||
Purpose: Start one task in an installed repository.
|
||||
|
||||
1. The body is only the native GitHub test sequence.
|
||||
2. The instructions explain that GitHub's existing issue or pull request is the task boundary.
|
||||
3. There is one action: open GitHub and perform the test.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open GitHub:** Opens an installed repository while Paperclip waits for the first signed mention to complete setup.
|
||||
|
||||
Rationale: A real mention proves the App installation without a separate verification screen.
|
||||
|
||||
### 47 · Connect an existing GitHub App
|
||||
|
||||
Purpose: Update the App in GitHub, then provide its identity credentials.
|
||||
|
||||
1. The copy control provides the exact values the operator must paste into GitHub.
|
||||
2. The instructions list every provider change required for an existing App.
|
||||
3. Only App ID and private key return to Paperclip; the generated webhook secret is already stored.
|
||||
4. Verification happens as part of Connect rather than on another screen.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Copy Paperclip webhook settings:** Copies the endpoint URL and generated webhook secret needed in the existing GitHub App settings.
|
||||
- **Connect and verify:** Stores the PEM file write-only, authenticates as the App, and verifies webhook, events, and least-privilege permissions.
|
||||
- **Back:** Returns to the credential-free App Manifest path.
|
||||
|
||||
Rationale: Existing Apps lack the manifest callback, so this advanced page contains the complete minimum manual configuration.
|
||||
|
||||
### 29 · GitHub overview
|
||||
|
||||
Purpose: Identity, health, capabilities, and lifecycle.
|
||||
|
||||
1. The endpoint keeps one Paperclip agent and one provider-native bot identity together.
|
||||
2. Installation and delivery health are summarized before any configuration detail.
|
||||
3. Every safe capability available to this provider is included automatically; this is status, not a set of switches.
|
||||
4. Test, pause, reconnect, and remove remain ordinary connector lifecycle actions.
|
||||
|
||||
Rationale: Overview remains provider-specific and outside onboarding.
|
||||
|
||||
### 17 · GitHub settings
|
||||
|
||||
Purpose: Scope, task boundaries, and necessary provider operations.
|
||||
|
||||
1. Repository and conversation-surface reach are the only content-scope choices.
|
||||
2. Existing GitHub objects supply the issue boundary; optional non-mention activation remains an explicit workflow choice.
|
||||
3. Host, private-key rotation, and installation drift are operational settings. GitHub response capabilities are reported on Overview and demonstrated in the walkthrough, never configured here.
|
||||
|
||||
Rationale: Settings remains provider-specific and outside onboarding.
|
||||
|
||||
### 30 · GitHub access
|
||||
|
||||
Purpose: Identity links, sponsored guests, and effective authority.
|
||||
|
||||
1. The endpoint sponsor supplies the maximum authority available to unlinked external people.
|
||||
2. Linked provider identities act as their current Paperclip users and retain ordinary permission checks.
|
||||
3. Unlinked people use the restricted sponsored-guest profile and cannot perform governance actions.
|
||||
4. Provider identity and scope details make effective authority explainable and auditable.
|
||||
|
||||
Rationale: Access remains provider-specific and outside onboarding.
|
||||
|
||||
### 31 · GitHub conversations
|
||||
|
||||
Purpose: Native conversation-to-Paperclip task bindings.
|
||||
|
||||
1. Each row names the provider-native conversation boundary and its single Paperclip issue.
|
||||
2. Participants, assigned agent, state, and last activity make live bindings scannable.
|
||||
3. Open in provider and Open task take an operator to either side of the binding.
|
||||
4. Detach preserves history and publication records; a later activation creates or claims a new binding.
|
||||
|
||||
Rationale: Conversations remains provider-specific and outside onboarding.
|
||||
|
||||
### 32 · GitHub activity
|
||||
|
||||
Purpose: Provider health, deliveries, publications, and retries.
|
||||
|
||||
1. Provider, credential, callback, and deployment-selected delivery health are summarized in one operational section.
|
||||
2. Inbound deliveries, callbacks, and outbound publications share a durable chronological ledger.
|
||||
3. Operators can inspect redacted errors and replay only safe, authorized failed deliveries.
|
||||
4. Rate limits, permission drift, uninstall or revocation, and provider-specific diagnostics stay visible.
|
||||
|
||||
Rationale: Activity remains provider-specific and outside onboarding.
|
||||
|
||||
### 18 · How GitHub conversations work
|
||||
|
||||
Purpose: The provider-native interaction and fallback model.
|
||||
|
||||
1. Ari mentions the bot in an existing GitHub issue, PR conversation, or inline review thread.
|
||||
2. Maya acknowledges with a reaction and one GitHub-Flavored Markdown comment rather than opening another thread.
|
||||
3. Paperclip binds that exact GitHub object or review thread to one assigned issue; PR conversation and inline review stay distinct.
|
||||
4. Later comments continue the same issue, while bot-authored comments and duplicate deliveries are ignored.
|
||||
5. Progress edits the existing comment; files and governed actions use authenticated Paperclip links.
|
||||
|
||||
Rationale: Capabilities are demonstrated here, not configured during setup.
|
||||
|
||||
### 19 · Create Maya for Microsoft Teams
|
||||
|
||||
Purpose: Run one command to register Maya with Microsoft.
|
||||
|
||||
1. The generated command is the only normal-path configuration artifact.
|
||||
2. Both instructions are actions the operator performs locally or in Microsoft's login.
|
||||
3. The manual path is available without exposing Azure choices on the default screen.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Copy setup command:** Copies a one-time Paperclip command that invokes Microsoft's Teams Developer CLI, signs the operator in, creates the Teams App and bot registration, and sends the resulting identity to this setup draft.
|
||||
- **Set up Microsoft manually:** Opens the Azure/Teams manual fallback for tenants that cannot run the guided command.
|
||||
|
||||
Rationale: The helper collapses Microsoft registration into one attended command while Microsoft remains the authority for sign-in and tenant policy.
|
||||
|
||||
### 49 · Install Maya in Microsoft Teams
|
||||
|
||||
Purpose: Open the Microsoft install page and add the app.
|
||||
|
||||
1. The install link replaces package download and upload on the normal path.
|
||||
2. The body contains only the two actions performed in Microsoft Teams.
|
||||
3. Tenant approval is handled by Microsoft's install experience, not another Paperclip choice.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Install Maya in Teams:** Opens the install link returned by Microsoft. Tenant policy may route the same request to an administrator for approval.
|
||||
|
||||
Rationale: Microsoft's CLI returns an install link, so normal setup should use it directly.
|
||||
|
||||
### 50 · Try Maya in Microsoft Teams
|
||||
|
||||
Purpose: Start one task in a channel post.
|
||||
|
||||
1. The body is only the Teams channel test sequence.
|
||||
2. The instructions teach the channel-post-and-replies task boundary.
|
||||
3. There is one action: open Teams and perform the test.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open Microsoft Teams:** Opens Teams while Paperclip waits for the first authenticated mention and reply to complete setup.
|
||||
|
||||
Rationale: The final provider event is the verification; no installation report is shown first.
|
||||
|
||||
### 48 · Set up Microsoft manually
|
||||
|
||||
Purpose: Create the bot in Microsoft, then paste the three identity values.
|
||||
|
||||
1. The copy control provides the one Paperclip value required by Microsoft.
|
||||
2. Every instruction is a portal operation the tenant administrator must perform.
|
||||
3. The three fields are the minimum identity values Paperclip needs to send as the bot.
|
||||
4. Connect verifies the identity and produces the same install step as the default flow.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Copy Paperclip endpoint:** Copies the public messaging endpoint that must be entered on the Azure Bot resource.
|
||||
- **Connect and create Teams app:** Stores the client secret write-only, verifies Microsoft bot authentication, and creates the installable Teams app and install link.
|
||||
- **Back:** Returns to the guided one-command setup.
|
||||
|
||||
Rationale: The manual fallback is longer because Microsoft has no manifest callback equivalent; no optional Azure choices are exposed.
|
||||
|
||||
### 33 · Microsoft Teams overview
|
||||
|
||||
Purpose: Identity, health, capabilities, and lifecycle.
|
||||
|
||||
1. The endpoint keeps one Paperclip agent and one provider-native bot identity together.
|
||||
2. Installation and delivery health are summarized before any configuration detail.
|
||||
3. Every safe capability available to this provider is included automatically; this is status, not a set of switches.
|
||||
4. Test, pause, reconnect, and remove remain ordinary connector lifecycle actions.
|
||||
|
||||
Rationale: Overview remains provider-specific and outside onboarding.
|
||||
|
||||
### 20 · Microsoft Teams settings
|
||||
|
||||
Purpose: Scope, task boundaries, and necessary provider operations.
|
||||
|
||||
1. Tenant, installed team/channel, personal, and group-chat reach are explicit scope choices.
|
||||
2. Channel threads and linear-conversation active tasks are different, visible issue boundaries.
|
||||
3. Bot identity, RSC, Graph consent, and installation drift are the only provider-level operations. Teams capabilities are reported on Overview and demonstrated in the walkthrough, never configured here.
|
||||
|
||||
Rationale: Settings remains provider-specific and outside onboarding.
|
||||
|
||||
### 34 · Microsoft Teams access
|
||||
|
||||
Purpose: Identity links, sponsored guests, and effective authority.
|
||||
|
||||
1. The endpoint sponsor supplies the maximum authority available to unlinked external people.
|
||||
2. Linked provider identities act as their current Paperclip users and retain ordinary permission checks.
|
||||
3. Unlinked people use the restricted sponsored-guest profile and cannot perform governance actions.
|
||||
4. Provider identity and scope details make effective authority explainable and auditable.
|
||||
|
||||
Rationale: Access remains provider-specific and outside onboarding.
|
||||
|
||||
### 35 · Microsoft Teams conversations
|
||||
|
||||
Purpose: Native conversation-to-Paperclip task bindings.
|
||||
|
||||
1. Each row names the provider-native conversation boundary and its single Paperclip issue.
|
||||
2. Participants, assigned agent, state, and last activity make live bindings scannable.
|
||||
3. Open in provider and Open task take an operator to either side of the binding.
|
||||
4. Detach preserves history and publication records; a later activation creates or claims a new binding.
|
||||
|
||||
Rationale: Conversations remains provider-specific and outside onboarding.
|
||||
|
||||
### 36 · Microsoft Teams activity
|
||||
|
||||
Purpose: Provider health, deliveries, publications, and retries.
|
||||
|
||||
1. Provider, credential, callback, and deployment-selected delivery health are summarized in one operational section.
|
||||
2. Inbound deliveries, callbacks, and outbound publications share a durable chronological ledger.
|
||||
3. Operators can inspect redacted errors and replay only safe, authorized failed deliveries.
|
||||
4. Rate limits, permission drift, uninstall or revocation, and provider-specific diagnostics stay visible.
|
||||
|
||||
Rationale: Activity remains provider-specific and outside onboarding.
|
||||
|
||||
### 21 · How Microsoft Teams conversations work
|
||||
|
||||
Purpose: The provider-native interaction and fallback model.
|
||||
|
||||
1. Ari mentions Maya in a new Teams channel post; that post and its replies are the native thread.
|
||||
2. Maya acknowledges under the post. If the installed permissions cannot deliver unmentioned replies, the bot says to mention Maya again.
|
||||
3. Paperclip creates one assigned issue and records tenant, team/channel, thread, and external participant attribution.
|
||||
4. Replies, files, and Adaptive Card or task-module actions continue only when current Teams delivery and Paperclip permissions allow.
|
||||
5. DMs may stream natively; channel and group output buffers or edits, with targeted-message, DM, or text-link fallback.
|
||||
|
||||
Rationale: Capabilities are demonstrated here, not configured during setup.
|
||||
|
||||
### 22 · Create Maya in Telegram
|
||||
|
||||
Purpose: Create the bot with BotFather and paste its token.
|
||||
|
||||
1. The page contains the exact three BotFather actions.
|
||||
2. The bot token is Telegram's only unavoidable setup input.
|
||||
3. The two buttons let the operator leave for BotFather and connect after returning.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open BotFather:** Opens Telegram's verified BotFather conversation so the operator can run /newbot.
|
||||
- **Connect bot:** Stores the token write-only, verifies the bot with getMe, and continues to the test step.
|
||||
|
||||
Rationale: Webhook, polling, commands, and identity checks are automatic and therefore absent.
|
||||
|
||||
### 51 · Try Maya in Telegram
|
||||
|
||||
Purpose: Send the bot its first message.
|
||||
|
||||
1. The minimum proof is one private message; group and forum reach can be added after connection.
|
||||
2. The body contains only the two Telegram actions required for the test.
|
||||
3. There is one action: open the bot and send the message.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open Maya in Telegram:** Opens the bot's t.me link while Paperclip waits for the first verified private message to complete setup.
|
||||
|
||||
Rationale: A private chat is Telegram's shortest path from BotFather token to a working Paperclip conversation.
|
||||
|
||||
### 37 · Telegram overview
|
||||
|
||||
Purpose: Identity, health, capabilities, and lifecycle.
|
||||
|
||||
1. The endpoint keeps one Paperclip agent and one provider-native bot identity together.
|
||||
2. Installation and delivery health are summarized before any configuration detail.
|
||||
3. Every safe capability available to this provider is included automatically; this is status, not a set of switches.
|
||||
4. Test, pause, reconnect, and remove remain ordinary connector lifecycle actions.
|
||||
|
||||
Rationale: Overview remains provider-specific and outside onboarding.
|
||||
|
||||
### 23 · Telegram settings
|
||||
|
||||
Purpose: Scope, task boundaries, and necessary provider operations.
|
||||
|
||||
1. Chat, topic, DM, and optional user reach are real scope choices.
|
||||
2. DM/group active tasks and forum-topic bindings make Telegram's non-Slack boundaries explicit.
|
||||
3. Delivery is read-only status; privacy mode and token rotation are the only provider operations exposed here. Telegram capabilities are reported on Overview and demonstrated in the walkthrough, never configured here.
|
||||
|
||||
Rationale: Settings remains provider-specific and outside onboarding.
|
||||
|
||||
### 38 · Telegram access
|
||||
|
||||
Purpose: Identity links, sponsored guests, and effective authority.
|
||||
|
||||
1. The endpoint sponsor supplies the maximum authority available to unlinked external people.
|
||||
2. Linked provider identities act as their current Paperclip users and retain ordinary permission checks.
|
||||
3. Unlinked people use the restricted sponsored-guest profile and cannot perform governance actions.
|
||||
4. Provider identity and scope details make effective authority explainable and auditable.
|
||||
|
||||
Rationale: Access remains provider-specific and outside onboarding.
|
||||
|
||||
### 39 · Telegram conversations
|
||||
|
||||
Purpose: Native conversation-to-Paperclip task bindings.
|
||||
|
||||
1. Each row names the provider-native conversation boundary and its single Paperclip issue.
|
||||
2. Participants, assigned agent, state, and last activity make live bindings scannable.
|
||||
3. Open in provider and Open task take an operator to either side of the binding.
|
||||
4. Detach preserves history and publication records; a later activation creates or claims a new binding.
|
||||
|
||||
Rationale: Conversations remains provider-specific and outside onboarding.
|
||||
|
||||
### 40 · Telegram activity
|
||||
|
||||
Purpose: Provider health, deliveries, publications, and retries.
|
||||
|
||||
1. Provider, credential, callback, and deployment-selected delivery health are summarized in one operational section.
|
||||
2. Inbound deliveries, callbacks, and outbound publications share a durable chronological ledger.
|
||||
3. Operators can inspect redacted errors and replay only safe, authorized failed deliveries.
|
||||
4. Rate limits, permission drift, uninstall or revocation, and provider-specific diagnostics stay visible.
|
||||
|
||||
Rationale: Activity remains provider-specific and outside onboarding.
|
||||
|
||||
### 24 · How Telegram conversations work
|
||||
|
||||
Purpose: The provider-native interaction and fallback model.
|
||||
|
||||
1. In a DM, Ari's first message creates the active issue; New task or /new deliberately starts another.
|
||||
2. In a privacy-on group, @maya starts work and replying to Maya continues; unrelated group traffic is not consumed.
|
||||
3. A forum topic can bind one issue through message_thread_id when the bot is present and allowed.
|
||||
4. Paperclip shows the active issue and makes the linear-chat boundary explicit instead of implying a Slack-style native thread.
|
||||
5. Maya uses throttled post/edit and inline buttons; unsupported or governed actions return text or DM with a Paperclip link.
|
||||
|
||||
Rationale: Capabilities are demonstrated here, not configured during setup.
|
||||
|
||||
### 11 · Externally bound task
|
||||
|
||||
Purpose: A normal Paperclip task with explicit publication and detach controls.
|
||||
|
||||
1. The task shows its external source.
|
||||
2. External actors remain attributed.
|
||||
3. Publishing back to the provider is explicit for human comments.
|
||||
4. The agent remains locked until detach.
|
||||
|
||||
Rationale: External work stays in the ordinary governed task experience.
|
||||
|
||||
### 12 · Agent Channels
|
||||
|
||||
Purpose: See every provider identity representing this agent.
|
||||
|
||||
1. Channel identities are summarized per provider.
|
||||
2. Health and recent tasks remain visible.
|
||||
3. Connections open in Connectors.
|
||||
4. Connect a channel preselects this agent.
|
||||
|
||||
Rationale: Agent detail summarizes endpoints while Connectors manages them.
|
||||
|
|
@ -0,0 +1,558 @@
|
|||
# Paperclip Chat Adapters UI Surfaces — v7
|
||||
|
||||
> Historical snapshot. The current permission, Access, and Conversations design is [v8](./2026-09-04-chat-adapters-ui-surfaces-v8.md). Generated wireframes are in the [Git archive](./wireframes-archive.md). Managed-install and helper-first concepts below are not shipped requirements.
|
||||
|
||||
Date: 2026-09-04
|
||||
Paperclip base: `7b094724e65c04949706df638d497afb02c84b62`
|
||||
Historical review viewer: [Git archive](./wireframes-archive.md).
|
||||
Archived wireframes: [v7 SVG snapshot](https://github.com/paperclipai/paperclip/tree/1c4a45f0ef7d627aa98e4f3ae3116d4507386d1a/doc/plans/chat-adapters/wireframes-v7) ([archive and regeneration notes](./wireframes-archive.md))
|
||||
|
||||
## Product decision
|
||||
|
||||
Overview is removed. Activated connectors open on Settings and expose only four management tabs: Settings, Access, Conversations, and Activity. Settings contains only destination reach that a user can plausibly change.
|
||||
|
||||
- **Channel activation:** A root mention creates a provider-native thread and one Paperclip task on Slack and Teams. Replies in that thread continue the same task without another mention.
|
||||
- **Existing provider thread:** The first mention inside an unbound Slack or Teams thread binds that existing thread to one new Paperclip task. Earlier messages are not imported automatically.
|
||||
- **Direct messages:** One open task is active in a DM. A completed task stays closed; the next message starts a new task. New task or /new starts another task explicitly.
|
||||
- **GitHub conversations:** A mention binds the addressed issue, pull-request conversation, or inline review thread to one Paperclip task.
|
||||
- **Telegram conversations:** DMs and ordinary groups use one active task. A forum topic has one stable topic-to-task binding.
|
||||
- **Delivery:** Paperclip chooses direct verified webhooks when reachable and the instance relay when private. This is deployment configuration, not an endpoint preference.
|
||||
- **Credentials and drift:** Invalid credentials, revoked installs, missing membership, or permission drift appear in Activity with a reconnect or repair action. They are not ordinary settings.
|
||||
|
||||
## Settings inventory
|
||||
|
||||
- Slack: allowed channels and an Allow direct messages toggle.
|
||||
- GitHub: allowed repositories only.
|
||||
- Microsoft Teams: allowed channels, Allow direct messages, and Allow group chats.
|
||||
- Telegram: allowed groups/topics and an Allow direct messages toggle.
|
||||
|
||||
## Screen inventory
|
||||
|
||||
| ID | Group | Surface | Title | Desktop | Mobile |
|
||||
| --- | --------------- | ------------------------ | -------------------------------------- | --------- | -------- |
|
||||
| 01 | Start | Shared | Connectors | 1280×800 | 375×812 |
|
||||
| 02 | Start | Shared | Choose how to connect | 1280×800 | 375×812 |
|
||||
| 03 | Start | Shared | Which agent do you want to chat with? | 1280×800 | 375×812 |
|
||||
| 13 | Slack | Setup | Add Maya to Slack | 1280×800 | 375×812 |
|
||||
| 42 | Slack | Custom setup | Create and install the Slack app | 1280×800 | 375×1064 |
|
||||
| 43 | Slack | Custom setup | Connect the Slack app | 1280×800 | 375×1176 |
|
||||
| 41 | Slack | Setup | Try Maya in Slack | 1280×800 | 375×944 |
|
||||
| 14 | Slack | Settings | Slack settings | 1280×800 | 375×936 |
|
||||
| 26 | Slack | Access | Slack access | 1280×1256 | 375×1592 |
|
||||
| 27 | Slack | Conversations | Slack conversations | 1280×1160 | 375×1600 |
|
||||
| 28 | Slack | Activity | Slack activity | 1280×1200 | 375×1640 |
|
||||
| 15 | Slack | Conversation walkthrough | How Slack conversations work | 1280×960 | 375×1320 |
|
||||
| 16 | GitHub | Setup | Create Maya in GitHub | 1280×800 | 375×952 |
|
||||
| 45 | GitHub | Setup | Choose GitHub repositories | 1280×800 | 375×1000 |
|
||||
| 46 | GitHub | Setup | Try Maya in GitHub | 1280×800 | 375×1000 |
|
||||
| 47 | GitHub | Custom setup | Connect an existing GitHub App | 1280×960 | 375×1392 |
|
||||
| 17 | GitHub | Settings | GitHub settings | 1280×800 | 375×812 |
|
||||
| 30 | GitHub | Access | GitHub access | 1280×1256 | 375×1592 |
|
||||
| 31 | GitHub | Conversations | GitHub conversations | 1280×1160 | 375×1600 |
|
||||
| 32 | GitHub | Activity | GitHub activity | 1280×1200 | 375×1640 |
|
||||
| 18 | GitHub | Conversation walkthrough | How GitHub conversations work | 1280×960 | 375×1320 |
|
||||
| 19 | Microsoft Teams | Setup | Create Maya for Microsoft Teams | 1280×800 | 375×1080 |
|
||||
| 49 | Microsoft Teams | Setup | Install Maya in Microsoft Teams | 1280×800 | 375×888 |
|
||||
| 50 | Microsoft Teams | Setup | Try Maya in Microsoft Teams | 1280×800 | 375×1000 |
|
||||
| 48 | Microsoft Teams | Custom setup | Set up Microsoft manually | 1280×1064 | 375×1496 |
|
||||
| 20 | Microsoft Teams | Settings | Microsoft Teams settings | 1280×800 | 375×968 |
|
||||
| 34 | Microsoft Teams | Access | Microsoft Teams access | 1280×1256 | 375×1592 |
|
||||
| 35 | Microsoft Teams | Conversations | Microsoft Teams conversations | 1280×1160 | 375×1600 |
|
||||
| 36 | Microsoft Teams | Activity | Microsoft Teams activity | 1280×1200 | 375×1640 |
|
||||
| 21 | Microsoft Teams | Conversation walkthrough | How Microsoft Teams conversations work | 1280×960 | 375×1320 |
|
||||
| 22 | Telegram | Setup | Create Maya in Telegram | 1280×800 | 375×1128 |
|
||||
| 51 | Telegram | Setup | Try Maya in Telegram | 1280×800 | 375×832 |
|
||||
| 23 | Telegram | Settings | Telegram settings | 1280×800 | 375×952 |
|
||||
| 38 | Telegram | Access | Telegram access | 1280×1256 | 375×1592 |
|
||||
| 39 | Telegram | Conversations | Telegram conversations | 1280×1160 | 375×1600 |
|
||||
| 40 | Telegram | Activity | Telegram activity | 1280×1200 | 375×1640 |
|
||||
| 24 | Telegram | Conversation walkthrough | How Telegram conversations work | 1280×960 | 375×1320 |
|
||||
| 11 | Paperclip | Task | Externally bound task | 1280×800 | 375×812 |
|
||||
| 12 | Paperclip | Agent | Agent Channels | 1280×800 | 375×812 |
|
||||
|
||||
## Annotation and action notes
|
||||
|
||||
### 01 · Connectors
|
||||
|
||||
Purpose: Connect tools and places where people talk to agents.
|
||||
|
||||
1. The existing Apps catalog remains the entry point.
|
||||
2. Filters separate chat and tool methods.
|
||||
3. Each connector row has one Connect action.
|
||||
4. Connection state remains visible in the catalog.
|
||||
|
||||
Rationale: The current Connectors surface remains canonical.
|
||||
|
||||
### 02 · Choose how to connect
|
||||
|
||||
Purpose: Shown for every connector that supports both chat and tool methods.
|
||||
|
||||
1. The existing connection wizard shell and selected provider are reused.
|
||||
2. Chat with an agent is the incoming-conversation path.
|
||||
3. Use this connection as an agent tool is the outbound tool/credential path.
|
||||
4. Single-purpose providers skip the choice.
|
||||
|
||||
Rationale: The registry drives the same direction choice for every dual-surface connector.
|
||||
|
||||
### 03 · Which agent do you want to chat with?
|
||||
|
||||
Purpose: Choose the one agent represented by this connection.
|
||||
|
||||
1. The existing agent selector is reused.
|
||||
2. Only active agents can be selected.
|
||||
3. One selection is required.
|
||||
4. Continue begins provider setup.
|
||||
|
||||
Rationale: This is the only shared Paperclip-specific setup decision.
|
||||
|
||||
### 13 · Add Maya to Slack
|
||||
|
||||
Purpose: Install Maya in your Slack workspace.
|
||||
|
||||
1. The step rail is the only repeated setup context; the selected agent is not restated in the page body.
|
||||
2. The page contains only the installation action and the necessary customer-owned-App fallback.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Add Maya to Slack:** Opens Slack's Add to Slack flow. The operator chooses a workspace and approves the installation; Slack then returns to the Try Maya step.
|
||||
- **Set up a custom Slack app:** Opens the customer-owned Slack App instructions for self-hosted deployments or organizations that cannot use Add to Slack.
|
||||
|
||||
Rationale: Nothing else on this page requires operator attention.
|
||||
|
||||
### 42 · Create and install the Slack app
|
||||
|
||||
Purpose: Paperclip prepared a Slack App Manifest for Maya.
|
||||
|
||||
1. Every line is an action the operator must complete in Slack.
|
||||
2. The manifest removes manual scope, event, callback, command, and interactivity configuration.
|
||||
3. The page advances only after the operator confirms the app was installed.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open Slack app setup:** Opens Slack's official app-from-manifest URL with Paperclip's generated manifest encoded in the link.
|
||||
- **Continue after installing:** Advances to the two credential fields after the operator has installed the new app in Slack.
|
||||
|
||||
Rationale: The custom path gives exact provider instructions without exposing Paperclip's automatic configuration.
|
||||
|
||||
### 43 · Connect the Slack app
|
||||
|
||||
Purpose: Copy two values from the Slack app settings.
|
||||
|
||||
1. The only help text tells the operator exactly where to find each required value.
|
||||
2. Only the two unavoidable Slack credentials are requested.
|
||||
3. Connecting verifies the values instead of showing a separate verification report.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Connect Slack app:** Stores both values write-only and verifies the Slack bot identity and required scopes before continuing.
|
||||
- **Back:** Returns to the Slack creation instructions without saving partially entered values.
|
||||
|
||||
Rationale: A customer-owned Slack App cannot return these values to Paperclip, so both fields are necessary.
|
||||
|
||||
### 41 · Try Maya in Slack
|
||||
|
||||
Purpose: Start one task and reply to it once.
|
||||
|
||||
1. The body is only the three actions needed to test the real Slack interaction.
|
||||
2. The instructions teach the root-mention-to-thread Paperclip task boundary.
|
||||
3. There is one action: open Slack and perform the test.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open Slack:** Opens the installed workspace while Paperclip waits for the root mention and thread reply to complete setup.
|
||||
|
||||
Rationale: Installation health and automatic verification do not belong on an instruction screen.
|
||||
|
||||
### 14 · Slack settings
|
||||
|
||||
Purpose: Choose where people can start conversations with Maya.
|
||||
|
||||
1. The connector starts on Settings; the read-only Overview tab is removed.
|
||||
2. Workspace appears only as context on each allowed channel; allowed channels are the only Slack resource choice.
|
||||
3. Direct messages are one explicit on/off choice.
|
||||
4. Save persists only reach changes; thread boundaries, delivery, credentials, drift, and capabilities are absent.
|
||||
|
||||
Rationale: Only destination reach remains configurable; all conversation and delivery behavior is a product default.
|
||||
|
||||
### 26 · Slack access
|
||||
|
||||
Purpose: Identity links, sponsored guests, and effective authority.
|
||||
|
||||
1. The endpoint sponsor supplies the maximum authority available to unlinked external people.
|
||||
2. Linked provider identities act as their current Paperclip users and retain ordinary permission checks.
|
||||
3. Unlinked people use the restricted sponsored-guest profile and cannot perform governance actions.
|
||||
4. Provider identity and scope details make effective authority explainable and auditable.
|
||||
|
||||
Rationale: Identity and authority remain independently manageable.
|
||||
|
||||
### 27 · Slack conversations
|
||||
|
||||
Purpose: Native conversation-to-Paperclip task bindings.
|
||||
|
||||
1. Each row names the provider-native conversation boundary and its single Paperclip issue.
|
||||
2. Participants, assigned agent, state, and last activity make live bindings scannable.
|
||||
3. Open in provider and Open task take an operator to either side of the binding.
|
||||
4. Detach preserves history and publication records; a later activation creates or claims a new binding.
|
||||
|
||||
Rationale: Operators can inspect and detach durable bindings.
|
||||
|
||||
### 28 · Slack activity
|
||||
|
||||
Purpose: Health, deliveries, publications, and repair actions.
|
||||
|
||||
1. Provider, credential, callback, and deployment-selected delivery health are summarized in one operational section.
|
||||
2. Inbound deliveries, callbacks, and outbound publications share a durable chronological ledger.
|
||||
3. Operators can inspect redacted errors and replay only safe, authorized failed deliveries.
|
||||
4. Rate limits, permission drift, uninstall or revocation, and provider-specific diagnostics stay visible.
|
||||
|
||||
Rationale: Diagnostics and conditional repairs live here instead of Settings.
|
||||
|
||||
### 15 · How Slack conversations work
|
||||
|
||||
Purpose: The fixed provider-native interaction and fallback model.
|
||||
|
||||
1. Ari starts in a Slack channel with a root @maya mention; unrelated root messages do not start work.
|
||||
2. Maya acknowledges inside a Slack thread, making the thread—not the channel—the visible conversation boundary.
|
||||
3. Paperclip creates exactly one assigned issue and shows its Slack source, external participant, and publication state.
|
||||
4. Ari continues by replying in the same thread without another mention; files and actions remain in that context.
|
||||
5. Maya's safe progress and final answer publish in the thread; failures offer retry or a Paperclip link.
|
||||
|
||||
Rationale: The walkthrough explains automatic behavior without turning it into configuration.
|
||||
|
||||
### 16 · Create Maya in GitHub
|
||||
|
||||
Purpose: Create a dedicated GitHub App from Paperclip's prepared manifest.
|
||||
|
||||
1. Only the two choices GitHub presents during App creation are described.
|
||||
2. The normal action uses the GitHub App Manifest handoff; credentials never pass through the operator.
|
||||
3. The existing-App branch remains available without cluttering the default path.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Create in GitHub:** Posts Paperclip's App Manifest to GitHub. GitHub returns to Paperclip after creation, and Paperclip stores the returned App credentials.
|
||||
- **Use an existing GitHub App:** Opens the advanced path for an App the organization already owns.
|
||||
|
||||
Rationale: The manifest already fixes permissions, events, and webhook configuration.
|
||||
|
||||
### 45 · Choose GitHub repositories
|
||||
|
||||
Purpose: Install Maya where people should be able to mention it.
|
||||
|
||||
1. The screen contains only GitHub's installation decisions.
|
||||
2. Repository scope stays in GitHub's native approval UI.
|
||||
3. One button begins the complete provider-owned installation step.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Install in GitHub:** Opens GitHub's App installation page and returns the installation and selected repository IDs to Paperclip.
|
||||
|
||||
Rationale: There is no Paperclip form to duplicate GitHub's repository picker.
|
||||
|
||||
### 46 · Try Maya in GitHub
|
||||
|
||||
Purpose: Start one task in an installed repository.
|
||||
|
||||
1. The body is only the native GitHub test sequence.
|
||||
2. The instructions explain that GitHub's existing issue or pull request is the task boundary.
|
||||
3. There is one action: open GitHub and perform the test.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open GitHub:** Opens an installed repository while Paperclip waits for the first signed mention to complete setup.
|
||||
|
||||
Rationale: A real mention proves the App installation without a separate verification screen.
|
||||
|
||||
### 47 · Connect an existing GitHub App
|
||||
|
||||
Purpose: Update the App in GitHub, then provide its identity credentials.
|
||||
|
||||
1. The copy control provides the exact values the operator must paste into GitHub.
|
||||
2. The instructions list every provider change required for an existing App.
|
||||
3. Only App ID and private key return to Paperclip; the generated webhook secret is already stored.
|
||||
4. Verification happens as part of Connect rather than on another screen.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Copy Paperclip webhook settings:** Copies the endpoint URL and generated webhook secret needed in the existing GitHub App settings.
|
||||
- **Connect and verify:** Stores the PEM file write-only, authenticates as the App, and verifies webhook, events, and least-privilege permissions.
|
||||
- **Back:** Returns to the credential-free App Manifest path.
|
||||
|
||||
Rationale: Existing Apps lack the manifest callback, so this advanced page contains the complete minimum manual configuration.
|
||||
|
||||
### 17 · GitHub settings
|
||||
|
||||
Purpose: Choose the repositories where people can mention Maya.
|
||||
|
||||
1. The connector starts on Settings; the read-only Overview tab is removed.
|
||||
2. The account and App installation are fixed; repository reach is the only normal GitHub chat setting.
|
||||
3. Save persists the repository allowlist; private-key or installation repair begins from Activity only when needed.
|
||||
|
||||
Rationale: Only destination reach remains configurable; all conversation and delivery behavior is a product default.
|
||||
|
||||
### 30 · GitHub access
|
||||
|
||||
Purpose: Identity links, sponsored guests, and effective authority.
|
||||
|
||||
1. The endpoint sponsor supplies the maximum authority available to unlinked external people.
|
||||
2. Linked provider identities act as their current Paperclip users and retain ordinary permission checks.
|
||||
3. Unlinked people use the restricted sponsored-guest profile and cannot perform governance actions.
|
||||
4. Provider identity and scope details make effective authority explainable and auditable.
|
||||
|
||||
Rationale: Identity and authority remain independently manageable.
|
||||
|
||||
### 31 · GitHub conversations
|
||||
|
||||
Purpose: Native conversation-to-Paperclip task bindings.
|
||||
|
||||
1. Each row names the provider-native conversation boundary and its single Paperclip issue.
|
||||
2. Participants, assigned agent, state, and last activity make live bindings scannable.
|
||||
3. Open in provider and Open task take an operator to either side of the binding.
|
||||
4. Detach preserves history and publication records; a later activation creates or claims a new binding.
|
||||
|
||||
Rationale: Operators can inspect and detach durable bindings.
|
||||
|
||||
### 32 · GitHub activity
|
||||
|
||||
Purpose: Health, deliveries, publications, and repair actions.
|
||||
|
||||
1. Provider, credential, callback, and deployment-selected delivery health are summarized in one operational section.
|
||||
2. Inbound deliveries, callbacks, and outbound publications share a durable chronological ledger.
|
||||
3. Operators can inspect redacted errors and replay only safe, authorized failed deliveries.
|
||||
4. Rate limits, permission drift, uninstall or revocation, and provider-specific diagnostics stay visible.
|
||||
|
||||
Rationale: Diagnostics and conditional repairs live here instead of Settings.
|
||||
|
||||
### 18 · How GitHub conversations work
|
||||
|
||||
Purpose: The fixed provider-native interaction and fallback model.
|
||||
|
||||
1. Ari mentions the bot in an existing GitHub issue, PR conversation, or inline review thread.
|
||||
2. Maya acknowledges with a reaction and one GitHub-Flavored Markdown comment rather than opening another thread.
|
||||
3. Paperclip binds that exact GitHub object or review thread to one assigned issue; PR conversation and inline review stay distinct.
|
||||
4. Later comments continue the same issue, while bot-authored comments and duplicate deliveries are ignored.
|
||||
5. Progress edits the existing comment; files and governed actions use authenticated Paperclip links.
|
||||
|
||||
Rationale: The walkthrough explains automatic behavior without turning it into configuration.
|
||||
|
||||
### 19 · Create Maya for Microsoft Teams
|
||||
|
||||
Purpose: Run one command to register Maya with Microsoft.
|
||||
|
||||
1. The generated command is the only normal-path configuration artifact.
|
||||
2. Both instructions are actions the operator performs locally or in Microsoft's login.
|
||||
3. The manual path is available without exposing Azure choices on the default screen.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Copy setup command:** Copies a one-time Paperclip command that invokes Microsoft's Teams Developer CLI, signs the operator in, creates the Teams App and bot registration, and sends the resulting identity to this setup draft.
|
||||
- **Set up Microsoft manually:** Opens the Azure/Teams manual fallback for tenants that cannot run the guided command.
|
||||
|
||||
Rationale: The helper collapses Microsoft registration into one attended command while Microsoft remains the authority for sign-in and tenant policy.
|
||||
|
||||
### 49 · Install Maya in Microsoft Teams
|
||||
|
||||
Purpose: Open the Microsoft install page and add the app.
|
||||
|
||||
1. The install link replaces package download and upload on the normal path.
|
||||
2. The body contains only the two actions performed in Microsoft Teams.
|
||||
3. Tenant approval is handled by Microsoft's install experience, not another Paperclip choice.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Install Maya in Teams:** Opens the install link returned by Microsoft. Tenant policy may route the same request to an administrator for approval.
|
||||
|
||||
Rationale: Microsoft's CLI returns an install link, so normal setup should use it directly.
|
||||
|
||||
### 50 · Try Maya in Microsoft Teams
|
||||
|
||||
Purpose: Start one task in a channel post.
|
||||
|
||||
1. The body is only the Teams channel test sequence.
|
||||
2. The instructions teach the channel-post-and-replies task boundary.
|
||||
3. There is one action: open Teams and perform the test.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open Microsoft Teams:** Opens Teams while Paperclip waits for the first authenticated mention and reply to complete setup.
|
||||
|
||||
Rationale: The final provider event is the verification; no installation report is shown first.
|
||||
|
||||
### 48 · Set up Microsoft manually
|
||||
|
||||
Purpose: Create the bot in Microsoft, then paste the three identity values.
|
||||
|
||||
1. The copy control provides the one Paperclip value required by Microsoft.
|
||||
2. Every instruction is a portal operation the tenant administrator must perform.
|
||||
3. The three fields are the minimum identity values Paperclip needs to send as the bot.
|
||||
4. Connect verifies the identity and produces the same install step as the default flow.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Copy Paperclip endpoint:** Copies the public messaging endpoint that must be entered on the Azure Bot resource.
|
||||
- **Connect and create Teams app:** Stores the client secret write-only, verifies Microsoft bot authentication, and creates the installable Teams app and install link.
|
||||
- **Back:** Returns to the guided one-command setup.
|
||||
|
||||
Rationale: The manual fallback is longer because Microsoft has no manifest callback equivalent; no optional Azure choices are exposed.
|
||||
|
||||
### 20 · Microsoft Teams settings
|
||||
|
||||
Purpose: Choose where people can start conversations with Maya.
|
||||
|
||||
1. The connector starts on Settings; the read-only Overview tab is removed.
|
||||
2. Tenant and bot identity are fixed; the tenant appears only as context on allowed Teams channels.
|
||||
3. Personal and group chats are independent reach toggles.
|
||||
4. Save persists only reach changes; post boundaries, consent, delivery, credentials, and drift are absent.
|
||||
|
||||
Rationale: Only destination reach remains configurable; all conversation and delivery behavior is a product default.
|
||||
|
||||
### 34 · Microsoft Teams access
|
||||
|
||||
Purpose: Identity links, sponsored guests, and effective authority.
|
||||
|
||||
1. The endpoint sponsor supplies the maximum authority available to unlinked external people.
|
||||
2. Linked provider identities act as their current Paperclip users and retain ordinary permission checks.
|
||||
3. Unlinked people use the restricted sponsored-guest profile and cannot perform governance actions.
|
||||
4. Provider identity and scope details make effective authority explainable and auditable.
|
||||
|
||||
Rationale: Identity and authority remain independently manageable.
|
||||
|
||||
### 35 · Microsoft Teams conversations
|
||||
|
||||
Purpose: Native conversation-to-Paperclip task bindings.
|
||||
|
||||
1. Each row names the provider-native conversation boundary and its single Paperclip issue.
|
||||
2. Participants, assigned agent, state, and last activity make live bindings scannable.
|
||||
3. Open in provider and Open task take an operator to either side of the binding.
|
||||
4. Detach preserves history and publication records; a later activation creates or claims a new binding.
|
||||
|
||||
Rationale: Operators can inspect and detach durable bindings.
|
||||
|
||||
### 36 · Microsoft Teams activity
|
||||
|
||||
Purpose: Health, deliveries, publications, and repair actions.
|
||||
|
||||
1. Provider, credential, callback, and deployment-selected delivery health are summarized in one operational section.
|
||||
2. Inbound deliveries, callbacks, and outbound publications share a durable chronological ledger.
|
||||
3. Operators can inspect redacted errors and replay only safe, authorized failed deliveries.
|
||||
4. Rate limits, permission drift, uninstall or revocation, and provider-specific diagnostics stay visible.
|
||||
|
||||
Rationale: Diagnostics and conditional repairs live here instead of Settings.
|
||||
|
||||
### 21 · How Microsoft Teams conversations work
|
||||
|
||||
Purpose: The fixed provider-native interaction and fallback model.
|
||||
|
||||
1. Ari mentions Maya in a new Teams channel post; that post and its replies are the native thread.
|
||||
2. Maya acknowledges under the post. If the installed permissions cannot deliver unmentioned replies, the bot says to mention Maya again.
|
||||
3. Paperclip creates one assigned issue and records tenant, team/channel, thread, and external participant attribution.
|
||||
4. Replies, files, and Adaptive Card or task-module actions continue only when current Teams delivery and Paperclip permissions allow.
|
||||
5. DMs may stream natively; channel and group output buffers or edits, with targeted-message, DM, or text-link fallback.
|
||||
|
||||
Rationale: The walkthrough explains automatic behavior without turning it into configuration.
|
||||
|
||||
### 22 · Create Maya in Telegram
|
||||
|
||||
Purpose: Create the bot with BotFather and paste its token.
|
||||
|
||||
1. The page contains the exact three BotFather actions.
|
||||
2. The bot token is Telegram's only unavoidable setup input.
|
||||
3. The two buttons let the operator leave for BotFather and connect after returning.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open BotFather:** Opens Telegram's verified BotFather conversation so the operator can run /newbot.
|
||||
- **Connect bot:** Stores the token write-only, verifies the bot with getMe, and continues to the test step.
|
||||
|
||||
Rationale: Webhook, polling, commands, and identity checks are automatic and therefore absent.
|
||||
|
||||
### 51 · Try Maya in Telegram
|
||||
|
||||
Purpose: Send the bot its first message.
|
||||
|
||||
1. The minimum proof is one private message; group and forum reach can be added after connection.
|
||||
2. The body contains only the two Telegram actions required for the test.
|
||||
3. There is one action: open the bot and send the message.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open Maya in Telegram:** Opens the bot's t.me link while Paperclip waits for the first verified private message to complete setup.
|
||||
|
||||
Rationale: A private chat is Telegram's shortest path from BotFather token to a working Paperclip conversation.
|
||||
|
||||
### 23 · Telegram settings
|
||||
|
||||
Purpose: Choose where people can start conversations with Maya.
|
||||
|
||||
1. The connector starts on Settings; the read-only Overview tab is removed.
|
||||
2. Allowed groups and forum topics are the Telegram resource choice.
|
||||
3. Direct messages are one explicit on/off choice.
|
||||
4. Save persists only reach changes; task boundaries, privacy, delivery, token rotation, and health are absent.
|
||||
|
||||
Rationale: Only destination reach remains configurable; all conversation and delivery behavior is a product default.
|
||||
|
||||
### 38 · Telegram access
|
||||
|
||||
Purpose: Identity links, sponsored guests, and effective authority.
|
||||
|
||||
1. The endpoint sponsor supplies the maximum authority available to unlinked external people.
|
||||
2. Linked provider identities act as their current Paperclip users and retain ordinary permission checks.
|
||||
3. Unlinked people use the restricted sponsored-guest profile and cannot perform governance actions.
|
||||
4. Provider identity and scope details make effective authority explainable and auditable.
|
||||
|
||||
Rationale: Identity and authority remain independently manageable.
|
||||
|
||||
### 39 · Telegram conversations
|
||||
|
||||
Purpose: Native conversation-to-Paperclip task bindings.
|
||||
|
||||
1. Each row names the provider-native conversation boundary and its single Paperclip issue.
|
||||
2. Participants, assigned agent, state, and last activity make live bindings scannable.
|
||||
3. Open in provider and Open task take an operator to either side of the binding.
|
||||
4. Detach preserves history and publication records; a later activation creates or claims a new binding.
|
||||
|
||||
Rationale: Operators can inspect and detach durable bindings.
|
||||
|
||||
### 40 · Telegram activity
|
||||
|
||||
Purpose: Health, deliveries, publications, and repair actions.
|
||||
|
||||
1. Provider, credential, callback, and deployment-selected delivery health are summarized in one operational section.
|
||||
2. Inbound deliveries, callbacks, and outbound publications share a durable chronological ledger.
|
||||
3. Operators can inspect redacted errors and replay only safe, authorized failed deliveries.
|
||||
4. Rate limits, permission drift, uninstall or revocation, and provider-specific diagnostics stay visible.
|
||||
|
||||
Rationale: Diagnostics and conditional repairs live here instead of Settings.
|
||||
|
||||
### 24 · How Telegram conversations work
|
||||
|
||||
Purpose: The fixed provider-native interaction and fallback model.
|
||||
|
||||
1. In a DM, Ari's first message creates the active issue; New task or /new deliberately starts another.
|
||||
2. In a privacy-on group, @maya starts work and replying to Maya continues; unrelated group traffic is not consumed.
|
||||
3. A forum topic can bind one issue through message_thread_id when the bot is present and allowed.
|
||||
4. Paperclip shows the active issue and makes the linear-chat boundary explicit instead of implying a Slack-style native thread.
|
||||
5. Maya uses throttled post/edit and inline buttons; unsupported or governed actions return text or DM with a Paperclip link.
|
||||
|
||||
Rationale: The walkthrough explains automatic behavior without turning it into configuration.
|
||||
|
||||
### 11 · Externally bound task
|
||||
|
||||
Purpose: A normal Paperclip task with explicit publication and detach controls.
|
||||
|
||||
1. The task shows its external source.
|
||||
2. External actors remain attributed.
|
||||
3. Publishing back to the provider is explicit for human comments.
|
||||
4. The agent remains locked until detach.
|
||||
|
||||
Rationale: External work stays in the ordinary governed task experience.
|
||||
|
||||
### 12 · Agent Channels
|
||||
|
||||
Purpose: See every provider identity representing this agent.
|
||||
|
||||
1. Channel identities are summarized per provider.
|
||||
2. Health and recent tasks remain visible.
|
||||
3. Connections open in Connectors.
|
||||
4. Connect a channel preselects this agent.
|
||||
|
||||
Rationale: Agent detail summarizes endpoints while Connectors manages them.
|
||||
|
|
@ -0,0 +1,444 @@
|
|||
# Paperclip Chat Adapters UI Surfaces — v8
|
||||
|
||||
Date: 2026-09-04
|
||||
Original planning base: `d593463ab6394cd356bf27448ea28bad8cccf4ec`; release qualification records the exact tested revision separately.
|
||||
Historical viewer and wireframes: [Git archive](./wireframes-archive.md); generated images are excluded from the PR.
|
||||
|
||||
## Permission model
|
||||
|
||||
- **Provider availability:** Slack, Teams, and Telegram decide where the bot is installed or invited. GitHub decides which repositories belong to the App installation.
|
||||
- **Paperclip enablement:** Paperclip responds only in provider resources that a Paperclip administrator has enabled for this connection. Invitation or installation alone is not permission to create a task.
|
||||
- **Effective reach:** A message is eligible only when the provider delivers it, its resource is enabled in Paperclip, the connection is active, and the sender has authority for the requested action.
|
||||
- **Safe default:** The destination used for the successful setup test becomes the first enabled resource. Resources discovered later start disabled.
|
||||
|
||||
## Access tab
|
||||
|
||||
**Settings answers where the bot may work. Access answers who an external sender represents and what Paperclip authority applies.** A linked external identity acts as its mapped Paperclip user and is checked against current permissions on every action. An unlinked identity may be allowed under the fixed restricted profile: it can converse within enabled resources and attach safe files, but it cannot approve, change budgets, hire, manage permissions or connections, or reassign agents. The connection owner remains an internal audit and authority ceiling; it is not ordinary UI configuration.
|
||||
|
||||
## Conversations tab
|
||||
|
||||
Each provider has one plain list. Every row contains the external conversation, Paperclip task, current state, an Open-provider link, and Open task. There is no separate binding-management section or conversation-boundary explainer. If provider access disappears, the row becomes unavailable while its history remains inspectable.
|
||||
|
||||
The former "How conversations work" screens are removed. Provider-native activation and reply behavior remains implementation documentation, not a standalone product page.
|
||||
|
||||
## Five-provider implementation addendum — 2026-09-06
|
||||
|
||||
Discord now uses the same product shell even though the v8 generated wireframe inventory below predates that implementation. Its current UI contract is:
|
||||
|
||||
- **Setup:** enter a customer-owned Application ID, Server ID, and write-only bot token; inspect the server-pinned `bot`-scope install URL; connect; then enable and test one channel. No webhook URL, interactions key, slash command, managed provisioning, or delivery-mode choice appears.
|
||||
- **Settings:** show provider identity and only the plausible direct-message reach switch. Provider capabilities are automatic.
|
||||
- **Access:** list Discord text channels available to the installed bot, with Paperclip enablement as an independent narrower allowlist, plus linked numeric Discord-user identities and the unlinked-participation policy.
|
||||
- **Conversations:** show the Discord thread or DM generation, Paperclip task, current state, **Open Discord**, and **Open task**. There are no detach or rebinding controls.
|
||||
- **Activity:** show Gateway/runtime health, durable deliveries/publications, redacted provider failures, and contextual reconnect/rotation actions.
|
||||
|
||||
The linked v8 SVGs remain a four-provider visual-design artifact; they are not evidence that Discord is absent from the product or that Discord has passed live qualification. The live browser runbook and dated qualification result are the current five-provider acceptance sources.
|
||||
|
||||
## Screen inventory
|
||||
|
||||
| ID | Group | Surface | Title | Desktop | Mobile |
|
||||
|---|---|---|---|---|---|
|
||||
| 01 | Start | Shared | Connectors | 1280×800 | 375×812 |
|
||||
| 02 | Start | Shared | Choose how to connect | 1280×800 | 375×812 |
|
||||
| 03 | Start | Shared | Which agent do you want to chat with? | 1280×800 | 375×812 |
|
||||
| 13 | Slack | Setup | Connect a Slack app | 1280×800 | 375×812 |
|
||||
| 41 | Slack | Setup | Try Maya in Slack | 1280×800 | 375×944 |
|
||||
| 14 | Slack | Settings | Slack settings | 1280×984 | 375×1072 |
|
||||
| 26 | Slack | Access | Slack access | 1280×880 | 375×920 |
|
||||
| 27 | Slack | Conversations | Slack conversations | 1280×800 | 375×916 |
|
||||
| 28 | Slack | Activity | Slack activity | 1280×1200 | 375×1640 |
|
||||
| 16 | GitHub | Setup | Create or connect a GitHub App | 1280×920 | 375×1312 |
|
||||
| 46 | GitHub | Setup | Try Maya in GitHub | 1280×800 | 375×812 |
|
||||
| 17 | GitHub | Settings | GitHub settings | 1280×816 | 375×896 |
|
||||
| 30 | GitHub | Access | GitHub access | 1280×880 | 375×920 |
|
||||
| 31 | GitHub | Conversations | GitHub conversations | 1280×800 | 375×916 |
|
||||
| 32 | GitHub | Activity | GitHub activity | 1280×1200 | 375×1640 |
|
||||
| 19 | Microsoft Teams | Setup | Create Maya for Microsoft Teams | 1280×800 | 375×1080 |
|
||||
| 49 | Microsoft Teams | Setup | Install Maya in Microsoft Teams | 1280×800 | 375×888 |
|
||||
| 50 | Microsoft Teams | Setup | Try Maya in Microsoft Teams | 1280×800 | 375×1000 |
|
||||
| 48 | Microsoft Teams | Setup | Microsoft provider setup details | 1280×1064 | 375×1496 |
|
||||
| 20 | Microsoft Teams | Settings | Microsoft Teams settings | 1280×1064 | 375×1176 |
|
||||
| 34 | Microsoft Teams | Access | Microsoft Teams access | 1280×880 | 375×920 |
|
||||
| 35 | Microsoft Teams | Conversations | Microsoft Teams conversations | 1280×800 | 375×916 |
|
||||
| 36 | Microsoft Teams | Activity | Microsoft Teams activity | 1280×1200 | 375×1640 |
|
||||
| 22 | Telegram | Setup | Create Maya in Telegram | 1280×800 | 375×1128 |
|
||||
| 51 | Telegram | Setup | Try Maya in Telegram | 1280×800 | 375×832 |
|
||||
| 23 | Telegram | Settings | Telegram settings | 1280×984 | 375×1072 |
|
||||
| 38 | Telegram | Access | Telegram access | 1280×880 | 375×920 |
|
||||
| 39 | Telegram | Conversations | Telegram conversations | 1280×800 | 375×916 |
|
||||
| 40 | Telegram | Activity | Telegram activity | 1280×1200 | 375×1640 |
|
||||
| 11 | Paperclip | Task | Externally connected task | 1280×800 | 375×812 |
|
||||
| 12 | Paperclip | Agent | Agent Channels | 1280×800 | 375×812 |
|
||||
|
||||
## Annotation and action notes
|
||||
|
||||
### 01 · Connectors
|
||||
|
||||
Purpose: Connect tools and places where people talk to agents.
|
||||
|
||||
1. The existing Apps catalog remains the entry point.
|
||||
2. Filters separate chat and tool methods.
|
||||
3. Each connector row has one Connect action.
|
||||
4. Connection state remains visible in the catalog.
|
||||
|
||||
Rationale: The current Connectors surface remains canonical.
|
||||
|
||||
### 02 · Choose how to connect
|
||||
|
||||
Purpose: Shown for every connector that supports both chat and tool methods.
|
||||
|
||||
1. The existing connection wizard shell and selected provider are reused.
|
||||
2. Chat with an agent is the incoming-conversation path.
|
||||
3. Use this connection as an agent tool is the outbound tool/credential path.
|
||||
4. Single-purpose providers skip the choice.
|
||||
|
||||
Rationale: The registry drives the same direction choice for every dual-surface connector.
|
||||
|
||||
### 03 · Which agent do you want to chat with?
|
||||
|
||||
Purpose: Choose the one agent represented by this connection.
|
||||
|
||||
1. The existing agent selector is reused.
|
||||
2. Only active agents can be selected.
|
||||
3. One selection is required.
|
||||
4. Continue begins provider setup.
|
||||
|
||||
Rationale: This is the only shared Paperclip-specific setup decision.
|
||||
|
||||
### 13 · Connect a Slack app
|
||||
|
||||
Purpose: Bring your own Slack app using Paperclip's prepared manifest.
|
||||
|
||||
1. The prepared manifest and exact provider locations make the customer-owned App the complete required path.
|
||||
2. Only the Bot User OAuth Token and Signing Secret are entered, and both remain write-only.
|
||||
3. Managed Add to Slack is not shipped; a later convenience cannot gate this path or release.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Connect Slack app:** Stores the two write-only credentials and verifies the Slack bot identity and required scopes.
|
||||
- **Open Slack app settings:** Opens Slack's app-management page where the operator creates and installs the customer-owned App.
|
||||
|
||||
Rationale: Bring-your-own credentials are the complete shipped path; no managed installation is required or currently shown.
|
||||
|
||||
### 41 · Try Maya in Slack
|
||||
|
||||
Purpose: Start one task and reply to it once.
|
||||
|
||||
1. The body is only the three actions needed to test the real Slack interaction.
|
||||
2. The instructions teach the root-mention-to-thread Paperclip task boundary.
|
||||
3. There is one action: open Slack and perform the test.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open Slack:** Opens the installed workspace while Paperclip waits for the root mention and thread reply to complete setup.
|
||||
|
||||
Rationale: Installation health and automatic verification do not belong on an instruction screen.
|
||||
|
||||
### 14 · Slack settings
|
||||
|
||||
Purpose: Enable the Slack channels where Maya may create and continue tasks.
|
||||
|
||||
1. Only provider-available destinations appear here.
|
||||
2. Each toggle is Paperclip's independent allow or deny decision.
|
||||
3. The provider action changes availability; newly discovered destinations remain disabled.
|
||||
4. Private-conversation reach is an explicit Paperclip choice.
|
||||
|
||||
Rationale: Provider membership is the ceiling; Paperclip enablement is the narrower enforcement boundary.
|
||||
|
||||
### 26 · Slack access
|
||||
|
||||
Purpose: Decide how people are identified when they message Maya.
|
||||
|
||||
1. The only guest-policy choice is whether unlinked people may participate.
|
||||
2. The restricted profile permits task conversation but never Paperclip governance.
|
||||
3. Linked accounts map a stable Slack workspace ID + user ID to a Paperclip user and can be revoked.
|
||||
|
||||
Rationale: Settings controls where the bot works; Access controls who external people represent and which authority model applies.
|
||||
|
||||
### 27 · Slack conversations
|
||||
|
||||
Purpose: Conversations created through this connection.
|
||||
|
||||
1. The active row pairs one Slack conversation with its task, state, Open Slack, and Open task links.
|
||||
2. The waiting row keeps the same compact fields and actions.
|
||||
3. The completed row remains available as history with the same two links.
|
||||
|
||||
Rationale: Conversations is a plain cross-linking list, not a binding-management surface.
|
||||
|
||||
### 28 · Slack activity
|
||||
|
||||
Purpose: Health, deliveries, publications, and repair actions.
|
||||
|
||||
1. Provider, credential, callback, and deployment-selected delivery health are summarized in one operational section.
|
||||
2. Inbound deliveries, callbacks, and outbound publications share a durable chronological ledger.
|
||||
3. Operators can inspect redacted errors and replay only safe, authorized failed deliveries.
|
||||
4. Rate limits, permission drift, uninstall or revocation, and provider-specific diagnostics stay visible.
|
||||
|
||||
Rationale: Diagnostics and conditional repairs live here instead of Settings.
|
||||
|
||||
### 16 · Create or connect a GitHub App
|
||||
|
||||
Purpose: Bring your own dedicated GitHub App and verify it with Paperclip.
|
||||
|
||||
1. The customer-owned App path is the complete shipped setup; no managed App Manifest exchange is required.
|
||||
2. Paperclip generates the webhook secret and never returns it from normal endpoint reads.
|
||||
3. Grant Metadata read, Issues and Pull requests read/write, plus Issue comment and Pull request review comment events; installation lifecycle events are automatic.
|
||||
4. GitHub installation scope and Paperclip repository enablement remain independent reach controls.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Generate webhook secret:** Creates and stores the webhook secret, then exposes its one-time copy value.
|
||||
- **Open new GitHub App form:** Opens GitHub App registration; GitHub remains the authority for App ownership and repository installation.
|
||||
- **Connect and verify:** Authenticates with the App ID and private key, verifies the immutable App identity, required permissions and events, installation, and signed webhook ping.
|
||||
|
||||
Rationale: Bring-your-own App credentials are sufficient to ship and preserve one provider bot identity per Paperclip agent.
|
||||
|
||||
### 46 · Try Maya in GitHub
|
||||
|
||||
Purpose: Start one task in an installed repository.
|
||||
|
||||
1. The test uses the real GitHub issue or pull-request conversation boundary.
|
||||
2. The first addressed setup repository becomes enabled; other discovered repositories remain disabled.
|
||||
3. One external conversation maps to one Paperclip task.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open GitHub:** Opens an installed repository while Paperclip waits for the first signed mention and follow-up to complete setup.
|
||||
|
||||
Rationale: A signed provider round trip proves installation, reach, identity, and conversation continuity.
|
||||
|
||||
### 17 · GitHub settings
|
||||
|
||||
Purpose: Enable the repositories where Maya may respond to mentions.
|
||||
|
||||
1. Only provider-available destinations appear here.
|
||||
2. Each toggle is Paperclip's independent allow or deny decision.
|
||||
3. The provider action changes availability; newly discovered destinations remain disabled.
|
||||
|
||||
Rationale: Provider membership is the ceiling; Paperclip enablement is the narrower enforcement boundary.
|
||||
|
||||
### 30 · GitHub access
|
||||
|
||||
Purpose: Decide how people are identified when they mention Maya.
|
||||
|
||||
1. The only guest-policy choice is whether unlinked people may participate.
|
||||
2. The restricted profile permits task conversation but never Paperclip governance.
|
||||
3. Linked accounts map a stable GitHub host + numeric user ID to a Paperclip user and can be revoked.
|
||||
|
||||
Rationale: Settings controls where the bot works; Access controls who external people represent and which authority model applies.
|
||||
|
||||
### 31 · GitHub conversations
|
||||
|
||||
Purpose: Conversations created through this connection.
|
||||
|
||||
1. The active row pairs one GitHub conversation with its task, state, Open GitHub, and Open task links.
|
||||
2. The waiting row keeps the same compact fields and actions.
|
||||
3. The completed row remains available as history with the same two links.
|
||||
|
||||
Rationale: Conversations is a plain cross-linking list, not a binding-management surface.
|
||||
|
||||
### 32 · GitHub activity
|
||||
|
||||
Purpose: Health, deliveries, publications, and repair actions.
|
||||
|
||||
1. Provider, credential, callback, and deployment-selected delivery health are summarized in one operational section.
|
||||
2. Inbound deliveries, callbacks, and outbound publications share a durable chronological ledger.
|
||||
3. Operators can inspect redacted errors and replay only safe, authorized failed deliveries.
|
||||
4. Rate limits, permission drift, uninstall or revocation, and provider-specific diagnostics stay visible.
|
||||
|
||||
Rationale: Diagnostics and conditional repairs live here instead of Settings.
|
||||
|
||||
### 19 · Create Maya for Microsoft Teams
|
||||
|
||||
Purpose: Register a customer-owned Entra App and Azure Bot.
|
||||
|
||||
1. Paperclip provides the exact public messaging endpoint.
|
||||
2. The operator creates the single-tenant Entra App, Azure Bot, and Teams app in Microsoft.
|
||||
3. No provisioning helper is shipped or required for the customer-owned path.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Copy messaging endpoint:** Copies the public callback for Azure Bot configuration.
|
||||
- **Open Microsoft setup:** Opens Microsoft's provider-owned registration surfaces.
|
||||
- **Connect and verify:** Stores the client secret write-only and verifies the tenant and application identity.
|
||||
|
||||
Rationale: Bring-your-own credentials are the required portable setup path.
|
||||
|
||||
### 49 · Install Maya in Microsoft Teams
|
||||
|
||||
Purpose: Publish or upload the customer-owned app, then add it in Teams.
|
||||
|
||||
1. Microsoft owns app creation, packaging, publication, approval, and installation.
|
||||
2. Paperclip does not generate a complete Teams package or promise an install link.
|
||||
3. Tenant approval remains in Microsoft's install experience.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open Teams Developer Portal:** Opens the provider-owned app surface; tenant policy may require administrator approval.
|
||||
|
||||
Rationale: Customer-owned registration is required; Microsoft owns the app artifact and installation.
|
||||
|
||||
### 50 · Try Maya in Microsoft Teams
|
||||
|
||||
Purpose: Start one task in a channel post.
|
||||
|
||||
1. The body is only the Teams channel test sequence.
|
||||
2. The instructions teach the channel-post-and-replies task boundary.
|
||||
3. There is one action: open Teams and perform the test.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open Microsoft Teams:** Opens Teams while Paperclip waits for the authenticated mention and reply.
|
||||
|
||||
Rationale: The final provider event is the verification.
|
||||
|
||||
### 48 · Microsoft provider setup details
|
||||
|
||||
Purpose: Create the customer-owned bot and app, then paste the three identity values.
|
||||
|
||||
1. The endpoint is the one Paperclip-specific value required by Microsoft.
|
||||
2. Every instruction is a provider portal operation.
|
||||
3. The three identity fields are the minimum credentials Paperclip needs.
|
||||
4. Connect does not generate a Teams package or install link.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Copy Paperclip endpoint:** Copies the public messaging endpoint for the Azure Bot resource.
|
||||
- **Connect and verify:** Stores the client secret write-only and verifies Microsoft bot authentication.
|
||||
- **Back:** Returns to the primary customer-owned credential setup.
|
||||
|
||||
Rationale: This is reference detail for the complete required customer-owned path.
|
||||
|
||||
### 20 · Microsoft Teams settings
|
||||
|
||||
Purpose: Enable the Teams channels where Maya may create and continue tasks.
|
||||
|
||||
1. Only provider-available destinations appear here.
|
||||
2. Each toggle is Paperclip's independent allow or deny decision.
|
||||
3. The provider action changes availability; newly discovered destinations remain disabled.
|
||||
4. Private-conversation reach is an explicit Paperclip choice.
|
||||
|
||||
Rationale: Provider membership is the ceiling; Paperclip enablement is the narrower enforcement boundary.
|
||||
|
||||
### 34 · Microsoft Teams access
|
||||
|
||||
Purpose: Decide how people are identified when they message Maya.
|
||||
|
||||
1. The only guest-policy choice is whether unlinked people may participate.
|
||||
2. The restricted profile permits task conversation but never Paperclip governance.
|
||||
3. Linked accounts map a stable Microsoft tenant ID + Entra object ID to a Paperclip user and can be revoked.
|
||||
|
||||
Rationale: Settings controls where the bot works; Access controls who external people represent and which authority model applies.
|
||||
|
||||
### 35 · Microsoft Teams conversations
|
||||
|
||||
Purpose: Conversations created through this connection.
|
||||
|
||||
1. The active row pairs one Microsoft Teams conversation with its task, state, Open Teams, and Open task links.
|
||||
2. The waiting row keeps the same compact fields and actions.
|
||||
3. The completed row remains available as history with the same two links.
|
||||
|
||||
Rationale: Conversations is a plain cross-linking list, not a binding-management surface.
|
||||
|
||||
### 36 · Microsoft Teams activity
|
||||
|
||||
Purpose: Health, deliveries, publications, and repair actions.
|
||||
|
||||
1. Provider, credential, callback, and deployment-selected delivery health are summarized in one operational section.
|
||||
2. Inbound deliveries, callbacks, and outbound publications share a durable chronological ledger.
|
||||
3. Operators can inspect redacted errors and replay only safe, authorized failed deliveries.
|
||||
4. Rate limits, permission drift, uninstall or revocation, and provider-specific diagnostics stay visible.
|
||||
|
||||
Rationale: Diagnostics and conditional repairs live here instead of Settings.
|
||||
|
||||
### 22 · Create Maya in Telegram
|
||||
|
||||
Purpose: Create the bot with BotFather and paste its token.
|
||||
|
||||
1. The page contains the exact three BotFather actions.
|
||||
2. The bot token is Telegram's only unavoidable setup input.
|
||||
3. The two buttons let the operator leave for BotFather and connect after returning.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open BotFather:** Opens Telegram's verified BotFather conversation so the operator can run /newbot.
|
||||
- **Connect bot:** Stores the token write-only, verifies the bot with getMe, and continues to the test step.
|
||||
|
||||
Rationale: Webhook, polling, commands, and identity checks are automatic and therefore absent.
|
||||
|
||||
### 51 · Try Maya in Telegram
|
||||
|
||||
Purpose: Send the bot its first message.
|
||||
|
||||
1. The minimum proof is one private message; group and forum reach can be added after connection.
|
||||
2. The body contains only the two Telegram actions required for the test.
|
||||
3. There is one action: open the bot and send the message.
|
||||
|
||||
Actions:
|
||||
|
||||
- **Open Maya in Telegram:** Opens the bot's t.me link while Paperclip waits for the first verified private message to complete setup.
|
||||
|
||||
Rationale: A private chat is Telegram's shortest path from BotFather token to a working Paperclip conversation.
|
||||
|
||||
### 23 · Telegram settings
|
||||
|
||||
Purpose: Enable the Telegram chats and topics where Maya may create and continue tasks.
|
||||
|
||||
1. Only provider-available destinations appear here.
|
||||
2. Each toggle is Paperclip's independent allow or deny decision.
|
||||
3. The provider action changes availability; newly discovered destinations remain disabled.
|
||||
4. Private-conversation reach is an explicit Paperclip choice.
|
||||
|
||||
Rationale: Provider membership is the ceiling; Paperclip enablement is the narrower enforcement boundary.
|
||||
|
||||
### 38 · Telegram access
|
||||
|
||||
Purpose: Decide how people are identified when they message Maya.
|
||||
|
||||
1. The only guest-policy choice is whether unlinked people may participate.
|
||||
2. The restricted profile permits task conversation but never Paperclip governance.
|
||||
3. Linked accounts map a stable Telegram bot ID + numeric user ID to a Paperclip user and can be revoked.
|
||||
|
||||
Rationale: Settings controls where the bot works; Access controls who external people represent and which authority model applies.
|
||||
|
||||
### 39 · Telegram conversations
|
||||
|
||||
Purpose: Conversations created through this connection.
|
||||
|
||||
1. The active row pairs one Telegram conversation with its task, state, Open Telegram, and Open task links.
|
||||
2. The waiting row keeps the same compact fields and actions.
|
||||
3. The completed row remains available as history with the same two links.
|
||||
|
||||
Rationale: Conversations is a plain cross-linking list, not a binding-management surface.
|
||||
|
||||
### 40 · Telegram activity
|
||||
|
||||
Purpose: Health, deliveries, publications, and repair actions.
|
||||
|
||||
1. Provider, credential, callback, and deployment-selected delivery health are summarized in one operational section.
|
||||
2. Inbound deliveries, callbacks, and outbound publications share a durable chronological ledger.
|
||||
3. Operators can inspect redacted errors and replay only safe, authorized failed deliveries.
|
||||
4. Rate limits, permission drift, uninstall or revocation, and provider-specific diagnostics stay visible.
|
||||
|
||||
Rationale: Diagnostics and conditional repairs live here instead of Settings.
|
||||
|
||||
### 11 · Externally connected task
|
||||
|
||||
Purpose: A normal Paperclip task connected to its provider conversation.
|
||||
|
||||
1. The task shows its external source and provider link.
|
||||
2. External actors remain attributed.
|
||||
3. Eligible agent output shows publication status.
|
||||
4. Board comments remain internal unless Send to channel is selected.
|
||||
|
||||
Rationale: The agent assignment stays fixed for the lifetime of the external task; a different agent requires a new connection.
|
||||
|
||||
### 12 · Agent Channels
|
||||
|
||||
Purpose: See every provider identity representing this agent.
|
||||
|
||||
1. Channel identities are summarized per provider.
|
||||
2. Health and recent tasks remain visible.
|
||||
3. Connections open in Connectors.
|
||||
4. Connect a channel preselects this agent.
|
||||
|
||||
Rationale: Agent detail summarizes endpoints while Connectors manages them.
|
||||
|
|
@ -0,0 +1,312 @@
|
|||
# GitHub live qualification result — 2026-09-05
|
||||
|
||||
For the September 7 private-attachment limitation and live outbound task-notice
|
||||
check, see [media qualification](2026-09-07-media-live-qualification.md).
|
||||
|
||||
For September 8–9 native Luna conversations, private-file omission/pasted-text
|
||||
proof and deployment, use the
|
||||
[current qualification ledger](2026-09-08-chat-queue-and-webhook-repair.md).
|
||||
Historical rows below keep their original scopes. Server 70's new task-link →
|
||||
correct-task upload journey and remaining provider permutations are not yet
|
||||
qualified live.
|
||||
|
||||
> **Status: current App connection, signed Tailscale ingress, exact agent replies, ordered burst handling, and keep-open idle recovery are proven; full production qualification remains open.** The September 7 checkpoints below supersede the older login/credential gates and the intermediate unsolicited-recovery blocker.
|
||||
|
||||
## 2026-09-07 current live checkpoint
|
||||
|
||||
On `95cbbd08e`, the user-authorized PEM import connected **Paperclip Maya E2E
|
||||
0906** (App ID `4853886`, installation `159668881`) to endpoint
|
||||
`e516ceb3-397c-4a28-9640-1b2779515fb9`. The installation is restricted to two
|
||||
private disposable repositories. The operator's `cryppadotta` identity is
|
||||
linked to the local Board account through the private confirmation flow.
|
||||
|
||||
The App now sends signed webhooks through stable Tailscale Funnel origin
|
||||
`https://dottas-macbook-pro.tail29c1aa.ts.net:10000`. Only provider webhook
|
||||
ingress is public; the board remains local/private. The temporary Cloudflare
|
||||
tunnel was stopped after a real signed issue comment reached Paperclip.
|
||||
|
||||
- [Issue 1](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/issues/1)
|
||||
created CHA-1 before identity linking and received the expected safe guest
|
||||
refusal. That task retains its guest trust classification.
|
||||
- [Issue 2](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/issues/2)
|
||||
created CHA-2 after identity linking, with a receipt reaction. Its bundled
|
||||
Codex ACP process incorrectly reported an unsupported-model provider error
|
||||
as a completed assistant response, which was published to GitHub. This is a
|
||||
release-blocking defect at that checkpoint, not a successful answer. Commit
|
||||
`1325329e3` repairs the typed ACP terminal-error classification. The later
|
||||
response-selection repair described below is separately required.
|
||||
- [Disabled-repository issue 1](https://github.com/cryppadotta/paperclip-chat-e2e-disabled/issues/1#issuecomment-5571234021)
|
||||
produced a GitHub webhook response **200 / ignored**, with no Paperclip
|
||||
conversation or task. Provider installation access did not override the
|
||||
Paperclip allowlist.
|
||||
|
||||
At `2026-09-07T13:45Z`, on `1325329e3` plus the final-response selection,
|
||||
receipt, and scheduler working-tree changes, an unmentioned follow-up in
|
||||
issue 2 requested exactly `GH-LIVE-0907-ROUNDTRIP-OK`. Run
|
||||
`b7190e01-0176-4af7-a471-c1e013c2a015` succeeded and
|
||||
[bot comment 5571558895](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/issues/2#issuecomment-5571558895)
|
||||
contained exactly that response. The existing conversation and CHA-2 task
|
||||
were retained. The setup UI subsequently completed and the endpoint is now
|
||||
`active`.
|
||||
|
||||
The preceding live run had produced the correct model final but published an
|
||||
earlier internal bookkeeping comment instead. External-chat runs now publish
|
||||
only the runner-selected final; intermediate lifecycle comments remain
|
||||
internal. A yielded or missing final cannot fall back to an internal note.
|
||||
After the corrected reply, generic productive-task recovery incorrectly
|
||||
started an unsolicited extra run. That separate queue defect is under repair;
|
||||
the exact reply is not evidence that the entire interaction lifecycle passes.
|
||||
The narrow recovery fix subsequently passed the full 133-case process-recovery
|
||||
suite. A rapid three-message live test also retained all messages on CHA-2,
|
||||
coalesced the last two into one deferred wake, and returned exactly
|
||||
`DELTA EPSILON` without mixing Discord's distinct test words. Its two causal
|
||||
runs took roughly 78 and 15 seconds. A keep-open task retest is still needed
|
||||
to verify the recovery guard live, because this burst ended with the task done.
|
||||
|
||||
### Clean keep-open recovery qualification — 2026-09-07, 13:59 UTC
|
||||
|
||||
This checkpoint supersedes the pending keep-open retest above. On clean source
|
||||
revision `5bd9c0d55`, an unmentioned follow-up on the existing CHA-2 issue left
|
||||
the task deliberately `in_progress` and requested exactly
|
||||
`GITHUB-IDLE-WAIT-OK`. Run `c3335bdf-6a2e-49a5-82eb-8d31df92e4d0` ran from
|
||||
`13:59:33.398Z` through `13:59:39.464Z` and succeeded. GitHub
|
||||
[bot comment 5571729974](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/issues/2#issuecomment-5571729974)
|
||||
contained exactly that marker.
|
||||
|
||||
CHA-2 remained `in_progress` with its external conversation active for more
|
||||
than eight minutes after the terminal reply. No additional run appeared. This
|
||||
is live evidence that an idle, keep-open chat task is no longer mistaken for
|
||||
stranded productive work, while explicit inbound and queued work remain
|
||||
runnable. It supersedes the earlier checkpoint where generic recovery started
|
||||
an unsolicited run after a successful reply.
|
||||
|
||||
### PR and review-comment boundary qualification — 2026-09-07, 14:29 UTC
|
||||
|
||||
A live pull-request boundary check used disposable private
|
||||
[PR 3](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/pull/3),
|
||||
branch `qa/chat-review-0907`, commit
|
||||
`e5219350f17973895671f420c596de16852d1f10`, and the two-line file
|
||||
`chat-review-0907.txt`. No repository operation was delegated to the agent.
|
||||
|
||||
The PR's main conversation received human comment `5572099126` and one
|
||||
[bot reply `5572100025`](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/pull/3#issuecomment-5572100025)
|
||||
containing exactly `GH-PR-LEVEL-0907-OK`. Paperclip bound provider thread
|
||||
`github:cryppadotta/paperclip-chat-e2e-enabled:3` to conversation
|
||||
`6f313c48-e684-421f-a730-dd68112c1e2c` and task
|
||||
`5329b4bf-6b16-40d5-ad69-65bcbeac2ab3`. Run
|
||||
`0d57af6e-2351-4bf2-8736-1d61cc877e67` ran from `14:29:10.041Z` through
|
||||
`14:29:16.354Z`.
|
||||
|
||||
GitHub's current Files changed UI did not expose an actionable line-level
|
||||
comment control during this walkthrough. The test therefore used **Comment on
|
||||
this file** followed by **Add single comment**. Human review comment
|
||||
`3950666444` received one
|
||||
[bot reply `3950666803`](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/pull/3/changes#r3950666803)
|
||||
containing exactly `GH-PR-REVIEW-0907-OK`. Paperclip bound the distinct provider
|
||||
thread `github:cryppadotta/paperclip-chat-e2e-enabled:3:rc:3950666444` to
|
||||
conversation `241f99a5-54ff-4bef-a0e7-313d69bf72b2` and task
|
||||
`860c7878-f1a6-498d-995c-6feaa735eb27`. Run
|
||||
`78deae9a-eb13-4374-a272-645ef1aec2d1` ran from `14:32:16.599Z` through
|
||||
`14:33:29.285Z`. Its working publication at `14:32:17.750Z` and final
|
||||
publication at `14:33:30.445Z` both settled through provider message
|
||||
`3950666803` in one attempt, so progress-to-final used one edited comment rather
|
||||
than producing duplicates.
|
||||
|
||||
This proves that a real PR main conversation and a real GitHub review-comment
|
||||
thread on the same PR bind to different Paperclip conversations and tasks, and
|
||||
that both can return an exact agent response. It does **not** qualify a
|
||||
line-specific review comment: the exercised GitHub control was file-level. The
|
||||
review reply also appeared only after a page reload. Its roughly 73-second
|
||||
latency was dominated by a 72-second model turn (`ensure_session` was about
|
||||
433 ms), not Paperclip queueing or provider transport; the result was correct,
|
||||
but that wait remains a user-experience risk and prevents calling this path
|
||||
fully production-ready.
|
||||
|
||||
### Image and file boundary — 2026-09-07
|
||||
|
||||
GitHub's native comment composer does not deliver uploaded bytes to the App.
|
||||
It first hosts the upload and writes a reference into the comment body. In the
|
||||
current GitHub UI, an image may appear as an HTML `<img src="https://github.com/user-attachments/assets/…">`
|
||||
element rather than Markdown image syntax; a general file appears as a
|
||||
Markdown link to `https://github.com/user-attachments/files/…`. Paperclip
|
||||
retains a bounded set of safe HTTPS destinations in the normalized task text,
|
||||
but deliberately does not fetch or store those provider-hosted bytes. GitHub's
|
||||
[anonymized-URL rules](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls)
|
||||
also mean the URL itself can be a capability, so it remains ordinary external
|
||||
text rather than being republished as a Paperclip-owned attachment.
|
||||
|
||||
The inverse direction is also link-only. The GitHub App issue-comment and
|
||||
pull-review-comment APIs accept a Markdown body, but expose no attachment-byte
|
||||
upload field. Using GitHub CLI's `--attach` workaround would require repository
|
||||
push access, which is intentionally outside this chat connection's Issues and
|
||||
Pull requests permissions. Paperclip therefore must not claim that a checked
|
||||
Board file was uploaded to GitHub. It now publishes an explicit limitation and,
|
||||
only when the Board has a safe externally configured URL, an authenticated
|
||||
Paperclip task link. A private/local Board produces a private-task notice with
|
||||
no unusable localhost or webhook-ingress URL.
|
||||
|
||||
The task banner presents this provider-specific boundary before send: checked
|
||||
files remain on the Paperclip task, while GitHub receives the authenticated
|
||||
task link or the private-task notice. Focused adapter coverage exercises both
|
||||
GitHub's native HTML image form and Markdown file-link form while asserting
|
||||
that neither becomes a native attachment. Integration coverage asserts both
|
||||
outbound fallback variants and that no provider file bytes or storage reads
|
||||
occur. This is truthful link interoperability, not native GitHub file transfer.
|
||||
|
||||
A live issue-comment exercise then used GitHub's native upload UI with a known
|
||||
image and a 128-byte text fixture. Human comment `5572301393` contained the
|
||||
default HTML image reference plus the Markdown file link. Run
|
||||
`0c252a02-51cc-4aeb-b829-73865415070e` ran from approximately `14:44:27Z`
|
||||
through `14:46:37Z`. The
|
||||
[bot reply `5572302077`](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/issues/2#issuecomment-5572302077)
|
||||
did not fabricate either file's contents, which is the correct safety outcome,
|
||||
but said that no authorized GitHub connection was available and suggested a
|
||||
new connection request. That explanation is misleading: the GitHub **chat**
|
||||
connection was active and transported the hosted links, but it intentionally
|
||||
grants neither GitHub repository-tool authority nor credentials for fetching
|
||||
provider-hosted attachment bytes. Chat-origin guidance must state that precise
|
||||
boundary instead of implying the existing App is disconnected or requesting a
|
||||
duplicate chat connection. Until that wording is corrected and the optional
|
||||
separate-tool path is qualified, inbound GitHub media remains link-preservation
|
||||
evidence, not readable-file qualification.
|
||||
|
||||
Focused adapter regression coverage now includes unmentioned follow-ups in PR
|
||||
and review-comment threads plus the native review reply/edit HTTP boundary.
|
||||
That file passed **3/3**, and the server typecheck passed. The broader
|
||||
current-tree integration result remains pending after the latest causal issue
|
||||
fence, so the earlier full-suite count is not advanced by this checkpoint.
|
||||
|
||||
The current split ingress topology keeps the board private. Public HTTPS
|
||||
`:10000` remains available for the existing Slack and GitHub callback URLs;
|
||||
public HTTPS `:8443` is the canonical webhook-only origin used for Telegram.
|
||||
Both terminate at the narrow loopback proxy on port 3104. HTTPS `:443` remains
|
||||
tailnet-only for the board, and the public webhook listeners do not forward
|
||||
board health or company API routes.
|
||||
|
||||
After the latest setup-edge changes, the full chat integration suite passed
|
||||
**258/258** and the combined process-recovery/status-payload suite passed
|
||||
**135/135**, both with zero skips. The deterministic browser suite had passed
|
||||
**5/5** on clean revision `5bd9c0d55`, but has not yet been rerun after the
|
||||
latest setup-edge/UI changes; the current working tree is therefore not being
|
||||
claimed browser-green here.
|
||||
|
||||
The [live addendum](2026-09-06-live-qualification-addendum.md) records exact
|
||||
delivery and runtime evidence. Broader burst/fault coverage, recovery, reviews/PRs,
|
||||
actions/files, and the rest of the release matrix remain open. All checkpoints
|
||||
below are historical, not descriptions of the current login or credential state.
|
||||
|
||||
## Historical 2026-09-06 evidence checkpoint
|
||||
|
||||
The evidence boundary is unchanged but is now quantified more precisely:
|
||||
|
||||
- The archived endpoint `4e87c64e-7d0b-497d-85d2-6eb8820340fc` is genuine historical transport proof. One GitHub issue mapped to one Paperclip task; two inbound issue comments were recorded; an exact webhook redelivery folded into the existing delivery; and six outbound publications reached GitHub in one attempt each.
|
||||
- The repository used for that historical proof was deleted during its authorized cleanup. Its former provider URL now returns HTTP 404, so it cannot be opened as current visual evidence and must not be cited as proof of the present source revision.
|
||||
- Four agent runs in that historical task failed closed because the principal was unlinked and the instance had no low-trust isolation environment. That is a Paperclip governance boundary, not a GitHub transport failure, and it must not be presented as successful agent execution.
|
||||
- The current draft endpoint whose id begins `a31` contains only a Paperclip-generated webhook secret. It has no verified GitHub App identity, private key, installation, repository, signed ping, conversation, or task.
|
||||
- Current setup is stopped at GitHub's **Confirm access** MFA challenge. That is an external account gate, not an implementation defect. Current-source live qualification cannot resume until the account owner completes that challenge and creates/installs the disposable App.
|
||||
|
||||
### Release decision at this checkpoint
|
||||
|
||||
GitHub remains a release blocker for the five-provider claim. The current browser session is still stopped at the six-digit sudo-mode MFA prompt, before App creation, key generation, installation, signed ping, or any issue/PR/review webhook. Deterministic browser, integration, signature, lifecycle, concurrency, and permission tests establish implementation coverage only; they do not convert the historical deleted-repository run into current-source provider evidence. A temporary tunnel response would prove only that Paperclip's route is reachable, not that a durable production callback, GitHub App identity, or real event round trip is qualified.
|
||||
|
||||
## Historical setup-run evidence and blocker
|
||||
|
||||
- Last pre-merge setup-attempt source revision: `77ad5383e3a8badf7b1b0933a7e9c66469186d55`
|
||||
- Latest implementation revision covered by focused checks: `83018c688`
|
||||
- Signed setup-ping, one-time secret generation, App-identity, lifecycle, admission, and runtime hardening are committed in the current branch.
|
||||
|
||||
The current endpoint is back in the honest pre-connect state: `draft`, at the provider-setup step, with no App identity, App ID, private key, installation, resource, conversation, delivery, publication, or signed setup ping recorded. This is expected because the GitHub App has not been created yet.
|
||||
|
||||
### Pre-connect secret trap found and healed
|
||||
|
||||
The live setup attempt exposed a control-plane defect before GitHub credentials existed. Regenerating Paperclip's webhook secret was treated as rotation of a configured App, which moved the endpoint to `attention` and asked the operator to reconnect credentials that had never been supplied. That was a false degraded state, not a provider failure.
|
||||
|
||||
The committed fix distinguishes first-time setup from live credential rotation:
|
||||
|
||||
1. Paperclip generates a random 32-byte webhook secret server-side, vaults it through endpoint-owned secret references, returns the plaintext once from the board-authenticated setup-secret route, and marks the response `Cache-Control: no-store`.
|
||||
2. Normal endpoint reads expose only `webhookSecretConfigured`; they never return the secret. The setup UI presents a read-only one-time copy value, then shows only configured state after refresh.
|
||||
3. Generating or replacing a secret before any App identity/App credentials exist keeps—or heals—the endpoint to `draft` / provider setup with unchecked connection health. It clears any verification for the superseded secret but does not pretend a live App was degraded.
|
||||
4. Rotating the secret after an App is configured remains fail-closed: it disables the runtime and requires the operator to update GitHub and reconnect.
|
||||
5. Every generation is audited as `chat_endpoint.setup_secret_generated` with safe metadata indicating whether the operation was a live rotation; no plaintext secret enters the activity record.
|
||||
6. The UI opens GitHub's new-App form for first setup, requires App ID and private key rather than pretending a secret-only endpoint is reusable, and explains the consequence before a real rotation.
|
||||
|
||||
The signed setup-ping path also accepts a correctly signed GitHub `ping` before App API credentials exist, records `chat_endpoint.webhook_verified` with only the safe provider delivery ID, and returns 401 for a missing or invalid signature. These were code and local-test results at the September 6 checkpoint; that App had not yet been created to send the ping.
|
||||
|
||||
## September 6 hardening checkpoint
|
||||
|
||||
The branch includes the following GitHub safety and concurrency behavior. These are code and local-test observations, not live GitHub qualification:
|
||||
|
||||
1. **Immutable App identity:** Paperclip binds the endpoint to the numeric App registration identity returned by GitHub, separately from the operator-entered App ID used to sign the App JWT. Reconnect and first-setup recovery from `attention` both revalidate an already claimed identity; credentials for a different App are rejected with `chat_bot_identity_changed`, including after a crash between identity claim and secret persistence.
|
||||
2. **Signed setup-ping state and UI gating:** only a `ping` whose `X-Hub-Signature-256` validates against the current Paperclip-generated webhook secret sets `webhookVerifiedAt`. Missing or invalid signatures return HTTP 401. The setup UI polls this safe timestamp, displays waiting/verified state, and keeps **Connect and verify** disabled until the signed ping has arrived.
|
||||
3. **Fail-closed secret rotation:** generating a replacement webhook secret clears the prior verification timestamp, removes the active runtime, degrades/disables the connection, and returns setup to the provider-update step. Reconnect remains blocked until GitHub sends a correctly signed ping using the new secret. Concurrent rotation/reconnect paths are serialized so stale credentials cannot overwrite the rotated secret.
|
||||
4. **Atomic first-resource admission:** the first addressed setup repository is admitted inside the endpoint's serialized transaction. Concurrent root mentions from two initially disabled repositories can enable only one repository and create only its one conversation/task; the other repository remains disabled rather than racing through the first-resource exception.
|
||||
5. **Runtime singleflight:** concurrent webhooks that arrive while a configured GitHub runtime is cold share one initialization promise. Paperclip installs one runtime and both requests proceed through it instead of racing duplicate adapter instances.
|
||||
6. **Complete repository inventory:** GitHub installation-repository discovery follows successive 100-item pages, so an installation with more than 100 repositories is not silently truncated. Installation discovery likewise scans every page before enforcing the one-active-installation invariant.
|
||||
7. **Retryable subscription without duplicate task state:** if the provider thread subscription fails after the task, external comment, wakeup request, and message link commit, the delivery remains retryable. A retry reuses those durable idempotent records, attempts the subscription again, and does not create another task, comment, or wakeup.
|
||||
8. **Lifecycle revalidation:** installation creation or unsuspension re-authenticates the exact stored App identity and rechecks required permissions and events before recovery. App-ID, permission, or event drift fails closed: the endpoint moves to attention, the connection/runtime is disabled, resources and conversations remain unavailable, and the lifecycle delivery stays diagnosable/retryable rather than restoring access optimistically.
|
||||
9. **Stable repository identity:** repository rename or transfer is reconciled through GitHub's immutable numeric repository ID. Paperclip preserves the resource, conversation, task, allowlist choice, and follow-up route while updating mutable owner/name coordinates and provider URLs; a conflicting dual-coordinate binding fails closed.
|
||||
10. **Cold-start response budget:** the provider ingress deadline begins before runtime initialization. A signed webhook that cannot finish cold adapter startup inside the provider budget returns promptly and proceeds only through bounded durable retry instead of consuming GitHub's delivery timeout before Paperclip begins accounting for it.
|
||||
11. **Provider-global App ownership:** the immutable numeric App registration
|
||||
id has one live Paperclip endpoint even if GitHub transfers the App to a
|
||||
different owner. Setup claims that id through a database uniqueness fence
|
||||
before persisting App credentials; concurrent cross-company attempts leave
|
||||
credentials only on the winner and do not reveal the owning company,
|
||||
endpoint, or agent.
|
||||
|
||||
None of these local checks substitutes for exercising the same paths against GitHub's real App registration, installation, webhook redelivery, and suspension UI.
|
||||
|
||||
On merge revision `da8f83d6c9befe7bf958f6d9cf12a95fc7e59e88`, the full chat-channel PostgreSQL integration suite passed 188/188 on fresh migrated database `chat_adapters_test_20260906_1140`; merge-conflict-focused server tests passed 355/355; and the deterministic browser suite `tests/e2e/chat-adapters-ui.spec.ts` passed 5/5 across Slack, GitHub, Teams, Discord, and Telegram. Implementation revision `83018c688` then passed the 42-test Discord adapter/runtime subset, the 34-test Discord/OpenAPI/UI contract subset, server/UI typechecks, token gates, a clean Discord patch application against the pristine package, and both working-tree checks. CI owns `pnpm-lock.yaml` and regenerates the PR lockfile artifact before its frozen install. Earlier provider-focused results remain valid regression evidence. These local results strengthen the setup path but do not change the live-provider blocker or qualification status.
|
||||
|
||||
The final combined working tree passed 193/193 chat-channel integration tests on fresh migrated database `chat_adapters_test_final_20260906_1257`, 111/111 focused runtime/error/privacy tests, all package typechecks, token gates, and the deterministic five-provider browser suite. This remains local evidence only for GitHub.
|
||||
|
||||
At the September 6 checkpoint, App registration and current-build provider delivery remained unexecuted because the signed-in session was stopped at GitHub's six-digit sudo-mode prompt. The later connected-App evidence above supersedes that setup gate without retroactively qualifying the unexecuted scenarios on the older revision.
|
||||
|
||||
## Historical-run scope
|
||||
|
||||
- Paperclip base used for the live run: `5da649986016e4010da8156f83f5bfc9c0128be4`
|
||||
- Reconciled release base after the run: `342c01fee`
|
||||
- Chat SDK / GitHub adapter: `4.39.0`
|
||||
- Provider: GitHub.com, disposable personal-account App and private repository
|
||||
- Paperclip endpoint: `4e87c64e-7d0b-497d-85d2-6eb8820340fc` (archived during cleanup)
|
||||
- External conversation: `github:cryppadotta/paperclip-chat-e2e-enabled:issue:1`
|
||||
- Paperclip task: `9ad34556-30b5-47a1-b207-ba666d8d897e`
|
||||
|
||||
No token, webhook secret, private key, cookie, password, or one-time identity-link URL is recorded here.
|
||||
|
||||
## Historical core-smoke result
|
||||
|
||||
The GitHub bring-your-own-App path passed the following core live round trip on `5da649986016e4010da8156f83f5bfc9c0128be4`:
|
||||
|
||||
1. Paperclip generated and stored the webhook secret without exposing it through normal endpoint reads.
|
||||
2. A private GitHub App was created with Issues and Pull requests set to read/write and only the selectable `issue_comment` and `pull_request_review_comment` events requested. GitHub supplied installation lifecycle events automatically.
|
||||
3. The App was installed on one selected private repository. Paperclip discovered that repository disabled by default.
|
||||
4. A mention sent before Paperclip access was enabled was durably filtered with `Destination is not enabled in Paperclip`.
|
||||
5. After enabling the repository, a root GitHub issue comment mentioning the immutable App bot created exactly one Paperclip conversation and one task.
|
||||
6. A non-mention follow-up in the same GitHub issue remained in the subscribed conversation.
|
||||
7. An explicit Paperclip board publication produced a GitHub bot reply and reached `published` state.
|
||||
8. The setup test completed with endpoint status `active` and health message `Connected`.
|
||||
|
||||
GitHub accepted all qualified webhook deliveries with HTTP 200 once a public relay was available. The initial Tailscale hostname was tailnet-only, so the run used a temporary TLS relay and then shut it down.
|
||||
|
||||
## Deviation
|
||||
|
||||
The isolated test instance had no sandbox workspace provider. Its automatic low-trust agent heartbeat therefore failed closed with `low_trust_isolation_unavailable`. The transport round trip was completed using the audited, explicit **Send to channel** publication path. This confirmed inbound mapping, subscribed replies, outbound provider delivery, and setup activation without weakening the low-trust containment invariant.
|
||||
|
||||
## Cleanup
|
||||
|
||||
- Closed the disposable GitHub issue.
|
||||
- Archived the Paperclip chat endpoint, which retired its endpoint-owned secrets but did not change any GitHub App registration, installation, repository grant, or webhook setting.
|
||||
- Separately deleted all four disposable GitHub Apps in GitHub after qualifying the provider form and manifest paths.
|
||||
- Deleted the explicitly disposable private repository `paperclip-chat-e2e-enabled`.
|
||||
- Stopped the temporary registration server, public relay, and isolated Paperclip process.
|
||||
|
||||
## Historical local regression evidence
|
||||
|
||||
- Workspace build: passed.
|
||||
- Shared, server, and UI typechecks: passed.
|
||||
- Focused shared/UI/OpenAPI tests: 45/45 passed.
|
||||
- Chat-channel PostgreSQL integration suite on fresh `chat_adapters_test_014`: 47/47 passed.
|
||||
- Deterministic browser suite `tests/e2e/chat-adapters-ui.spec.ts`: 4/4 passed.
|
||||
- Token gates and `git diff --check`: passed.
|
||||
|
||||
This evidence is useful for regression comparison, but it is incomplete release evidence. In particular, the full live runbook's issue/PR/inline-review boundary matrix, linked and unlinked identity authorization, reaction/edit lifecycle, text-only attachment fallback, burst/redelivery behavior, installation suspension/recovery, and all cleanup assertions were not all executed in this run. GitHub remains unqualified for stable release until the current source revision passes the complete live runbook.
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
# Slack live qualification result — 2026-09-05
|
||||
|
||||
For the later September 7 image/file handoff retake, see
|
||||
[media qualification](2026-09-07-media-live-qualification.md).
|
||||
|
||||
For the September 8–9 native Luna media, queue and deployment evidence, use the
|
||||
[current qualification ledger](2026-09-08-chat-queue-and-webhook-repair.md).
|
||||
The checkpoint rows below retain their original tested revisions; they do not
|
||||
qualify server 70 or the remaining live modal/Stop/governance permutations.
|
||||
|
||||
> **Status: broad current-branch live evidence plus historical core-smoke evidence, not full release qualification.** The current runs cover channel roots, DMs, FIFO follow-ups, reactions, edits, pause/resume, the registered Slack command, a command-created thread, native inbound and outbound files, disabled-resource enforcement and recovery, an interleaved command/status/final race, a complete native question-to-continuation round trip, and one revocation/relink sequence. Slack is still missing the rest of the governance, failure-injection, reinstall, and cleanup matrix.
|
||||
|
||||
## 2026-09-07 fresh-connection setup edge and recovery
|
||||
|
||||
The new `maya-e2e` app/endpoint `e3948092-3d92-46a5-9e19-525bd31a53eb`
|
||||
reproduced the user's first-message failure on the isolated live instance. `CHA-5`
|
||||
was admitted as an unlinked guest and failed closed with
|
||||
`low_trust_isolation_unavailable`. The safety boundary was correct; the setup
|
||||
experience was not: setup offered a test before explaining identity readiness,
|
||||
then showed both a failed-run toast and an agent-wide error with a UUID.
|
||||
|
||||
The repair explains identity readiness in the test step, provides **Review identity
|
||||
access** and a return **Continue setup** action, and never silently upgrades a
|
||||
previously admitted guest task. Recognized pre-adapter low-trust admission failures
|
||||
leave the healthy agent idle while preserving the failed run and safe external
|
||||
refusal. The isolated-workspace case now receives one actionable warning using
|
||||
the agent name. A failed refusal or a manually published Board comment cannot
|
||||
qualify setup: completion requires the assigned agent's succeeded run and a
|
||||
published final response.
|
||||
|
||||
Through the signed-in Slack and Paperclip interfaces, the observed `dotta` identity
|
||||
was privately linked to Board. A **fresh root mention** created `CHA-6`
|
||||
(`967ce77c-9b64-4f79-8e49-1bfc377ce908`). Run
|
||||
`09d577dd-97e7-42d4-a2e4-9aeacac909d4` succeeded from
|
||||
`14:06:01.690Z` to `14:06:08.599Z`, and Slack visibly showed exact
|
||||
[`SLACK-LINKED-0907-OK`](https://papercliplabs.slack.com/archives/C0BUT55N9RV/p1788789962495849?thread_ts=1788789960.341109&cid=C0BUT55N9RV).
|
||||
The task remained `in_progress`, waiting for external input without an unsolicited
|
||||
recovery run through the `14:20Z` observation. `CHA-5` remains quarantined historical
|
||||
failure evidence. The root receipt reaction was still visible; this retest does
|
||||
not claim new Slack receipt-removal coverage.
|
||||
|
||||
A plain, unmentioned thread reply on the combined server then passed on the same
|
||||
`CHA-6`: run `b3a2584b-ed42-4c3b-979b-427caf5b9267` succeeded from
|
||||
`14:23:26.401Z` to `14:23:32.423Z`, and Slack showed exact
|
||||
[`SLACK-THREAD-FOLLOWUP-0907-OK`](https://papercliplabs.slack.com/archives/C0BUT55N9RV/p1788791007411959?thread_ts=1788789960.341109&cid=C0BUT55N9RV).
|
||||
The real **I've sent the test message** action then completed the wizard and
|
||||
rendered **active** with no stale **Continue setup** action. The Settings page was
|
||||
visually inspected after navigation. This verifies the original setup recovery
|
||||
and the ordinary thread follow-up, not every notification animation or failure.
|
||||
|
||||
Source: `5bd9c0d55` plus the setup-edge repairs in this change. The live server was
|
||||
restarted on the combined working tree at `14:20:00Z`. Fresh-database chat
|
||||
integration passed **258/258**, recovery/status tests **135/135**, and focused UI
|
||||
tests **47/47**. These are supporting regressions, not a replacement for the
|
||||
remaining live runbook matrix or a comprehensive notification/transition audit.
|
||||
The September 6 sections below remain historical evidence on their named builds.
|
||||
|
||||
## 2026-09-06 identity-revocation and callback-drift retest
|
||||
|
||||
The live endpoint's linked Slack identity was temporarily revoked before a uniquely marked direct message. Because **Allow unlinked people** was enabled, Paperclip admitted the message only as an external guest and created `CHA-88` under the low-trust quarantine profile. Execution failed closed when the instance could not provide isolated guest execution, and Slack received the safe link/isolation notice. The revoked principal did not retain the Paperclip user's membership or governance authority.
|
||||
|
||||
Restoring the identity link did not silently upgrade the already-created low-trust task. That generation remained quarantined, which is the safe boundary: trust is fixed at admission rather than changing underneath an existing task. After starting a fresh Slack generation, Paperclip admitted the now-linked principal normally; `CHA-89` reached `done` and Slack displayed exact `SLACK-LINK-RESTORED-0906`. This is live proof of immediate revocation plus safe fresh-generation recovery for one linked principal. It is not proof of every role-demotion, unlinked-disabled, governed-action, or concurrent revocation race in C3.
|
||||
|
||||
The fresh-generation attempt also exposed an operational callback-drift failure. Tunnel rotation had updated and verified the Events API and Interactivity URLs, but Slack's registered slash-command Request URL still pointed at the retired hostname. `/maya-fdhjew new` therefore returned Slack's visible `dispatch_unknown_error` until that third URL was updated. After the command callback was repaired, the same flow created `CHA-89` and passed as described above.
|
||||
|
||||
This was configuration drift across three independently stored Slack callback surfaces, not a message-queue or agent failure. It is nevertheless a release risk: an account-less Cloudflare quick tunnel is not production ingress, and an operator can otherwise have healthy events and buttons while commands are broken. Production deployment requires a durable HTTPS origin and a callback-health workflow that verifies Events API, Interactivity, and the registered command together after any origin change.
|
||||
|
||||
## 2026-09-06 merged-build ingress and FIFO retest
|
||||
|
||||
The live-tested merge commit is `da8f83d6c9befe7bf958f6d9cf12a95fc7e59e88`. A first pair of direct messages sent against that build exposed the expected weakness of the temporary test ingress rather than an adapter failure: Cloudflare had retired the account-less quick-tunnel hostname, so Slack accepted the messages while Paperclip received no callbacks. After a new tunnel was created and Slack's Events API and Interactivity URLs were both re-verified, Slack's enabled delayed-event recovery delivered those two missed events. Paperclip processed each once and returned exact `SLACK-MERGED-A-0906` and `SLACK-MERGED-B-0906` responses in order. Later implementation revision `83018c688` changes only Discord log redaction plus documentation and setup copy relative to that tested Slack runtime.
|
||||
|
||||
A second pair sent 300 ms apart on the healthy ingress returned exact `SLACK-MERGED-C-0906` and `SLACK-MERGED-D-0906` responses in FIFO order. Because Slack DMs are a linear conversation, both messages intentionally used one active Paperclip task and serialized two agent turns. Each turn first published `Maya is working…` and then edited that same Slack message in place to the exact final. Across the four deliveries, the durable ledger contains four processed inbound rows and eight published rows (four working/final pairs), all with `attempts=1`, no error, and no pending, retry, failed, or ambiguous publication. This is positive recovery and queue evidence; the expired hostname confirms that a stable HTTPS origin is mandatory for production.
|
||||
|
||||
After the final combined hardening, all three Slack callback surfaces were rotated together to a fresh test origin and the current working tree was restarted on the migrated live database. `CHA-93` reached `done` and Slack displayed exact `SLACK-FINAL-SOURCE-2-0906`. Its inbound delivery processed once; working and final publications each completed with `attempts=1`, no error, and shared provider message id `1788717343.064939`. This is current-source smoke evidence for the Events API, slash-command reset, linked-principal execution, and in-place final publication. The account-less tunnel remains an explicitly non-production dependency.
|
||||
|
||||
## 2026-09-06 answer-handoff and channel-root retest
|
||||
|
||||
The later release-candidate working tree retained endpoint `2782e758-8e1e-47e3-a5aa-6a8359b1c23c` and repaired all three Slack callback surfaces after the development tunnel changed. Slack accepted the current Events API, Interactivity, and slash-command URLs, and delayed-event recovery remained enabled. This is useful current-provider evidence, but the temporary Cloudflare hostname is not a production ingress qualification; a stable deployment must keep a durable HTTPS origin across restarts.
|
||||
|
||||
The first DM answer retest (`CHA-82`) exposed a real handoff delay: an external answer stopped only native-mode source runs, so the legacy source continued until cancellation fallback and the continuation did not begin for about 23.5 seconds. The fix now cancels both native and legacy question-source runs and uses a compare-and-set terminal write so cancellation cannot overwrite a genuinely completed run.
|
||||
|
||||
The post-fix DM retest (`CHA-84`) showed the source run cancelled about 61 ms after the answer publication was created and the dedicated continuation queued about 93 ms after cancellation. Slack rendered exact `SLACK-HANDOFF8-Violet` about 20.8 seconds after the click; all five publications completed in one attempt, the task finished, and no generic or late duplicate followed. That elapsed time includes agent execution and publication, while the measured control-plane handoff itself remained sub-100 ms.
|
||||
|
||||
A separate enabled-channel root (`CHA-83`) produced one native Slack thread and one Paperclip task, then returned exact `SLACK-CHANNEL-FINAL-0906` in that thread with no cross-publication. The provider reply appeared about three seconds after the root. A live `+1` add and remove each produced one processed reaction delivery in roughly 3–4 ms of server handling, with `attempts=1` and no error.
|
||||
|
||||
## 2026-09-06 current-build interactive and reaction closure
|
||||
|
||||
An earlier pre-merge live retest ran the uncommitted release-candidate working tree based on `77ad5383e` after restarting the server with the same instance home and its then-current public webhook URL:
|
||||
|
||||
- Paperclip updated and Slack verified all three ingress surfaces: Events API, Interactivity, and `/maya-fdhjew`. A fresh direct message returned exact provider-visible response `SLACK-CURRENT-BUILD-0906`, and the registered `/maya-fdhjew status` command returned the current task without creating a task solely for the control.
|
||||
- A live question whose optional `allowOther` field was omitted initially degraded to a link-only card. The shared schema defines that field as optional, so omission must mean a closed question unless it is explicitly `true`. After the fix, the same natural request rendered native **Red** and **Blue** controls. Selecting **Red** changed the card to **Answered: Red**, scheduled one continuation, and produced exact provider-visible `SLACK-RETEST-Red`; the generic completion did not race or follow it.
|
||||
- A top-level DM reaction initially reached the Slack webhook but was discarded because the SDK supplied `slack:<DM>:<message-ts>` while Paperclip's linear DM binding is `slack:<DM>:`. The first fallback still chose the newest task generation and missed reactions on older linked messages. The final implementation resolves the exact owning generation through the durable message link, keeps the endpoint/reach/principal checks, and permits completed DM generations only for this audit-only event. A final live `+1` add and remove each produced one processed delivery; neither created a comment, task, run, wake, approval, or governed action.
|
||||
- Heartbeat's resolved final-assistant presentation is now externalized only when the run has an exact causal chat binding. Ordinary internal runs keep `internal_agent_write`; chat-origin and native-interaction continuation runs receive the narrow `allow_chat_run_presentation` reason. This closes the earlier continuation gap without exposing reasoning, tool traces, or logs.
|
||||
- When a chat-origin run creates a provider-visible native question or confirmation, that original prompt now consumes the run's external presentation slot. The run's meta-summary remains an internal Paperclip comment, the generic completion is suppressed, and only the distinct post-answer continuation may publish its final response. The rule keys on the exact source run and survives a fast-answer race.
|
||||
|
||||
## 2026-09-06 native file and action follow-up
|
||||
|
||||
Earlier provider checks on pre-merge revision `77ad5383e3a8badf7b1b0933a7e9c66469186d55` refined the evidence boundary:
|
||||
|
||||
- The disabled-resource negative and recovery path passed live. While the Slack resource was disabled in Paperclip, the provider message did not create a task or produce bot work. Restoring the permitted resource allowed a later request through without replacing the endpoint or losing its existing resource identity.
|
||||
- The older `CHA-68` attempt is **not** outbound-file proof. Its explicit publication delivered text, but the separately created attachment was not bound to that comment's publication lineage, so Slack never received the intended native file. This exposed an implementation defect in the Paperclip comment/attachment handoff rather than a Slack transport rejection.
|
||||
- After the explicit attachment binding fix, fresh task `CHA-71` passed the live outbound-file check. Paperclip bound the attachment to the explicit comment before publication; Slack then received the text followed by the native file, with each durable publication completing in one attempt.
|
||||
- On `CHA-70`, selecting **Blue** on the native question card was accepted exactly once, the unselected sibling action expired, and Paperclip scheduled exactly one continuation. That older attempt exposed the missing continuation lineage. The current-build **Red** retest documented above supersedes it: the accepted-state update and exact continuation response both completed without a generic completion.
|
||||
|
||||
## 2026-09-06 final live extension
|
||||
|
||||
The active endpoint `2782e758-8e1e-47e3-a5aa-6a8359b1c23c` added the following current-provider evidence:
|
||||
|
||||
- Slack accepted the manifest with reaction and lifecycle subscriptions. At `08:36:54Z`, one-attempt `group_left` delivery for `C0BUT55N9RV` marked the resource unavailable. At `08:37:33Z`, one-attempt `member_joined_channel` restored it, hydrated the label to `#pc-chat-live-0905b`, and preserved the operator's enabled choice. The self-removal subscription and label-preservation defects found here were fixed; this was not an account or permission gate.
|
||||
- Edits on `CHA-29` and `CHA-47` each produced one `message_updated` delivery and one internal system edit comment. Deleting the source message for `CHA-64` at `08:39:16Z` produced one one-attempt delivery and one internal deletion comment; deleted content was not republished.
|
||||
- Repeated natural and slash-command DM generations worked. The latest natural DM, `CHA-67`, processed once at `08:42Z`, showed the receipt reaction, and returned exact final `slack-dm-live-final-0906` in about two seconds. An idle `status` returned `No task active` without creating a task.
|
||||
- Inbound attachment proof includes the earlier 67-byte `text/plain` file on `CHA-52` and the current 41-byte file on `CHA-64`; both were persisted and read successfully, and the latter returned exact marker `paperclip-live-telegram-media-proof-0906`. The separate outbound proof remains `CHA-71`, where the explicitly bound Paperclip attachment reached Slack as a native file.
|
||||
- `CHA-42` received two replies 144 ms apart. The second run began only after the first succeeded, and each run retained its own coalesced placeholder/final message. Four reaction-add and four reaction-remove callbacks also processed once each.
|
||||
- Forty-five provider duplicate callbacks folded into 38 existing delivery rows without duplicate tasks or comments. All 97 earlier publications were `published`; all 44 runs after the isolation configuration succeeded. For the post-`05:00Z` sample, 24 processed inbound events averaged `0.702s` (`p50 0.781s`, `p95 1.146s`, maximum `1.646s`) and 25 publications averaged `0.488s` (`p50 0.315s`, `p95 1.103s`, maximum `1.127s`), all published.
|
||||
|
||||
The outbound-file and tested rich-interaction gaps are now closed. Broader modal/form behavior still needs live coverage. Earlier low-trust failures were governance isolation, and two old synthetic-command receipt warnings are preserved pre-fix evidence; neither is a current Slack account gate.
|
||||
|
||||
## September 5–6 source and evidence boundary
|
||||
|
||||
- Pre-merge source revision for the historical breadth checks below: `77ad5383e3a8badf7b1b0933a7e9c66469186d55`
|
||||
- Most recently live-rerun Slack source revision at that checkpoint: `da8f83d6c9befe7bf958f6d9cf12a95fc7e59e88`
|
||||
- Later implementation revision (Discord log redaction and documentation/setup-copy follow-up only): `83018c688`
|
||||
- The synthetic-command receipt, native thread binding, ordered task-control, coherent progress/status/final lane, explicit attachment binding, native-action lifecycle, final-presentation lineage, and top-level DM reaction-generation fixes are present in the final merge revision. The historical breadth checks exercised the pre-merge revision above; the merged-build section records the final live rerun.
|
||||
- Live checkpoint: 2026-09-05 through 2026-09-06
|
||||
- Live endpoint at that checkpoint: `2782e758-8e1e-47e3-a5aa-6a8359b1c23c`
|
||||
- Paperclip issue: `d7f718da-a8da-468e-99a7-79dc337d5cbc`
|
||||
|
||||
No bot token, signing secret, webhook URL, cookie, password, or one-time identity-link URL is recorded here.
|
||||
|
||||
The sections below deliberately distinguish provider-visible proof from durable database evidence and local-only regression coverage. A successful local test is not reported as a live Slack result.
|
||||
|
||||
## Latest live breadth run
|
||||
|
||||
The latest live run added the following provider and durable-ledger evidence:
|
||||
|
||||
1. A channel root requesting the exact response `slack-prod-root-0906` produced one admitted mention delivery, one Paperclip task, and the exact provider-visible final response.
|
||||
2. A normal DM requesting `slack-dm-prod-0906` produced one admitted direct-message delivery and one final response. Editing that source message produced one separate `message_updated` audit delivery and did not wake another agent run.
|
||||
3. The registered immutable command was exercised in the real D-prefixed Slack DM. A `status` control and a following `new` control each produced one processed delivery with `attempts=1` and no error after the first receipt fix. The durable normalized record explicitly says those synthetic callbacks do not support a receipt reaction; no task was created merely to acknowledge the controls.
|
||||
4. A command task requesting `slack-slash-task-a74` created exactly one `slash_task_start` action, one Slack starter message/thread, and one Paperclip task (`CHA-50`). Its working and final publications each completed in one attempt and shared the same provider message ID, so Slack showed one in-place final response rather than a progress/final duplicate.
|
||||
5. A reply in that Slack thread requesting `slack-thread-followup-a74` produced one inbound delivery with `attempts=1` and no error. Because the preceding DM task was already terminal, Paperclip advanced the linear DM binding to its next session generation; that generation produced one working/final pair, again using one provider message ID, and the exact response was visible once.
|
||||
6. A native file plus “Read the attached file and reply with exactly its Token value” produced one inbound delivery, one stored Paperclip issue attachment, and one final `chat-upload-a74` publication. The final publication completed in one attempt and replaced its working placeholder in place. This proves the tested Slack file-download and attachment-storage path for that file, not every Slack file type or size boundary.
|
||||
7. On the final revision, `/maya-fdhjew Run sleep 12 then reply exactly slack-status-lane-6f13b` created native Slack thread `CHA-61`. While the run was active, `/maya-fdhjew status` replaced the working reply with the current `in_progress` state. The final then replaced that same reply with `slack-status-lane-6f13b`. Slack showed exactly one bot reply beneath `Starting a task…`, not stale working/status siblings. The working, status, and final publication rows all share provider message ID `1788679967.804189`; each is `published`, `attempts=1`, with no error.
|
||||
|
||||
### Synthetic-command receipt defects found during the run
|
||||
|
||||
The live command work found two related but separate bugs rather than treating the first patch as sufficient:
|
||||
|
||||
1. The first real DM `status` callback was represented internally by a deterministic hash because Slack slash callbacks have no native message to react to. The generic receipt path nevertheless sent that hash to Slack as a message timestamp. Slack returned `message_not_found`, leaving the processed delivery with a receipt-reaction error even though the status response itself continued. The fix persists `acknowledgement.receiptReactionSupported=false`, carries it through deferred reconstruction, and skips the reaction. A later live `status` and `new` both processed once with no error, which is live proof for this control-command branch.
|
||||
2. The command-task branch had a second synthetic message after posting its real starter message. It still took the generic receipt path, so the otherwise successful `slack-slash-task-a74` delivery recorded the same `message_not_found` receipt error. The follow-up fix marks this branch unsupported too and adds regression coverage that Slack command callbacks never call `addReaction`, while Telegram commands retain their real provider message tuple and still do. The final live `CHA-61` command task and its interleaved status callback both processed in one attempt with no error and `receiptReactionSupported=false`, which is live proof of this second fix.
|
||||
|
||||
The earlier diagnostic rows remain preserved as bug evidence. The later clean rows, rather than rewriting history, provide the live regression proof.
|
||||
|
||||
### Post-run thread-binding and recovery audit
|
||||
|
||||
Reviewing the pinned Slack adapter after the live run exposed a third issue that the earlier fake runtime did not model: a slash command's `Channel` wrapper returns the channel wrapper id after a root post, while Slack's returned message timestamp is the actual native thread root. Treating the wrapper id as the task boundary can make later Paperclip publications appear as new top-level messages instead of replies under `Starting a task…`. The implementation now derives the canonical `slack:<channel>:<message timestamp>` thread id from the confirmed provider message and has a regression whose mock deliberately returns the non-thread channel wrapper id.
|
||||
|
||||
The same audit found that DM `status`, `new`, and `close` controls synthesized a base-DM thread id and therefore could not find a task created under the slash starter's native root. Those controls now resolve the most recently active task for that DM and route the synthetic control through its exact native thread binding.
|
||||
|
||||
Finally, an ambiguous starter post no longer remains an unactionable Activity row. Paperclip still never replays it automatically. Activity offers an audited **Retry anyway** only when the durable action contains complete reconstruction context, warns that both the starter and Paperclip task can duplicate, and offers **Cancel task start**. The retry revalidates the endpoint, destination, and original principal, serializes against endpoint mutation, and admits at most one concurrent retry. Older incomplete rows are cancel-only. Native thread binding and ordinary command creation were retested live; the deliberately ambiguous starter-recovery branch remains local-only because the provider failure was not injected live.
|
||||
|
||||
### Qualification-harness restart incident
|
||||
|
||||
The first final-revision command attempt returned Slack's “app did not respond” notice because the restarted local server was accidentally launched against the live database without its existing Paperclip instance home and encryption-key path. Secret resolution failed closed and no task was admitted. Restarting with the original instance home restored credential decryption, after which the same scenario passed. This is not a Slack adapter defect, but it is operational evidence that database restores and process restarts must preserve the Paperclip-generated master key; the database alone is intentionally insufficient.
|
||||
|
||||
## Earlier current-run evidence
|
||||
|
||||
A signed Slack root message reached the current public tunnel and completed the provider-visible lifecycle: the bot added its receipt reaction, showed a working response, and replaced or completed it with the successful final response in the originating thread.
|
||||
|
||||
Earlier unlinked-guest attempts reached Paperclip but failed closed at agent execution with `low_trust_isolation_unavailable`. After identity linking, the current root interaction completed successfully. The `037e57e0d` UX change now presents that containment failure as an actionable blocked-execution explanation instead of leaving the operator with a vague stopped-run state; this is a UX correction, not a relaxation of the low-trust isolation boundary.
|
||||
|
||||
The rapid two-message FIFO retest also passed:
|
||||
|
||||
1. Slack sent follow-up one and follow-up two in the same thread at `21:06:31.005` and `21:06:31.353` respectively.
|
||||
2. Paperclip processed each delivery exactly once, with `attempts=1` and no error, at `21:06:32.054` and `21:06:32.286`.
|
||||
3. All three runs succeeded with exit code 0 and were strictly non-overlapping. In UTC on 2026-09-06, the root ran from `02:05:50` to `02:06:04`, follow-up one from `02:06:32` to `02:06:44`, and follow-up two from `02:06:44.544` to `02:06:58`.
|
||||
4. Slack displayed the two final replies in one-then-two order.
|
||||
5. The root and two follow-ups produced six working/final publications total. Every publication completed with `attempts=1` and no error, and each working message was edited in place to its corresponding final response rather than producing an extra progress message.
|
||||
|
||||
This is direct evidence for single-thread FIFO serialization, exactly-once delivery processing in this burst, and working-to-final in-place edits. It does not replace the unexecuted Slack capability, governance, failure-injection, and recovery scenarios listed below.
|
||||
|
||||
The current public tunnel also passed a reaction round trip after the FIFO run. A user `+1` on follow-up two produced one `reaction_added` delivery for provider message `1788660391.353319`; removing it produced one `reaction_removed` delivery for the same message. Both were processed once with normalized `thumbs_up`/raw `+1` metadata and no redacted error. This proves the manifest's added reaction subscriptions are active on the current setup, not merely present in configuration.
|
||||
|
||||
### Latest false-duplicate and pause/resume retest
|
||||
|
||||
A subsequent pre-merge live retest produced the following evidence in UTC:
|
||||
|
||||
1. A root sent at `04:28:42` completed normally. Its durable delivery recorded one legitimate ignored duplicate caused by Slack exposing the same root through overlapping subscribed event shapes. This expected provider overlap remained deduplicated after removal of the separate false internal-drain duplicate counter.
|
||||
2. Follow-up one and follow-up two were sent at `04:29:27.713` and `04:29:27.857`. Each processed exactly once with `attempts=1`, no error, and `duplicateCount=0`.
|
||||
3. Their runs were FIFO and strictly non-overlapping: follow-up one ran from `04:29:28.507` to `04:29:50.777`, then follow-up two ran from `04:29:50.828` to `04:30:03.065`.
|
||||
4. Each working/final publication pair completed with `attempts=1` and no error. The final publication reused the working publication's provider message ID, so each response was edited in place and no duplicate external reply appeared.
|
||||
5. After the endpoint was paused at approximately `04:32`, `slack-paused-should-not-run` appeared in Slack but produced no bot reaction, no bot reply, and no Paperclip delivery. After resume, `slack-resume-ok` was accepted once and published one final response.
|
||||
|
||||
The later run also exposed a redundant automation follow-up wake inside Paperclip: the active run's own final comment carried `resume: true`, so it queued another wake even though the same run still owned the issue. The wake was deferred rather than run concurrently, and Slack received no duplicate external message, but the queue work was unnecessary. The fix now suppresses this narrow same-owning-run case while retaining explicit resume from a completed prior run.
|
||||
|
||||
A post-fix live retest at `04:58:32` sent `slack-no-empty-wake`. Paperclip admitted one message delivery, processed it once (`attempts=1`, no error), published working and final states once each by editing the same Slack provider message, and produced exactly one assignment wake for the incoming message. No automation follow-up wake was inserted by the agent's own final comment. Slack displayed one final `slack-no-empty-wake` reply.
|
||||
|
||||
## Pre-merge local regression evidence
|
||||
|
||||
- On that pre-merge working tree based on revision `77ad5383e`, the full chat-channel PostgreSQL integration suite passed 183/183 on fresh migrated database `chat_adapters_test_final_20260906_0833`.
|
||||
- Focused shared tests passed 11/11, focused server tests passed 194/194, and focused UI tests passed 41/41.
|
||||
- The deterministic browser suite `tests/e2e/chat-adapters-ui.spec.ts` passed 4/4, and shared, database, server, and UI typechecks all passed.
|
||||
- These deterministic checks support the live continuation and reaction fixes but do not replace the remaining provider cases.
|
||||
|
||||
## Historical-run scope
|
||||
|
||||
- Paperclip release base: `342c01fee`
|
||||
- Provider: Slack, disposable App and private channel in the Paperclip workspace
|
||||
- Paperclip endpoint: `c3c20e8d-5dbd-49b7-9d7e-14068c9ded8b`
|
||||
- External conversation: `slack:C0C0NFGUYKS`
|
||||
- Paperclip task: `4a6dd0ca-d022-44c6-868d-7246169f3ef4`
|
||||
|
||||
No bot token, signing secret, webhook URL, cookie, password, or one-time identity-link URL is recorded here.
|
||||
|
||||
## Historical core-smoke result
|
||||
|
||||
The Slack bring-your-own-App path passed the following core live round trip on `342c01fee`:
|
||||
|
||||
1. The Paperclip manifest created a Slack App with the exact 16 required bot scopes, including reaction read/write support.
|
||||
2. Slack accepted the Paperclip request URL for Events API delivery and interactivity.
|
||||
3. Paperclip rejected neither the App identity nor its scopes and advanced the endpoint to live verification.
|
||||
4. A root channel mention created one provider thread, one Paperclip conversation, and one Paperclip task assigned to the endpoint's immutable agent.
|
||||
5. An unmentioned reply in the Slack thread stayed in the same Paperclip conversation and task.
|
||||
6. Paperclip added the processing reaction, published lifecycle messages in the originating thread, and ignored Slack retry duplicates durably.
|
||||
7. An explicit **Send to channel** board publication produced a Slack bot reply in the same thread and reached `published` state.
|
||||
8. The setup test completed with endpoint status `active`, the discovered private channel enabled, and all Settings, Access, Conversations, and Activity views present.
|
||||
|
||||
The Activity view recorded both inbound deliveries as processed, showed provider retry duplicates ignored, and recorded all outbound messages as published.
|
||||
|
||||
## Deviation
|
||||
|
||||
The isolated test instance had no sandbox workspace provider. Its automatic low-trust agent heartbeat therefore failed closed with `low_trust_isolation_unavailable`. The transport round trip was completed using the audited, explicit **Send to channel** publication path. This confirmed inbound mapping, subscribed thread replies, outbound provider delivery, deduplication, and setup activation without weakening the low-trust containment invariant.
|
||||
|
||||
## Cleanup
|
||||
|
||||
- Archived the disposable private Slack channel.
|
||||
- Removed the Paperclip Slack endpoint, retiring its endpoint-owned secrets. This did not uninstall the Slack app or remove it from channels.
|
||||
- Separately deleted the disposable Slack App in Slack, revoking its bot token and signing secret.
|
||||
- Stopped the temporary public relay and isolated Paperclip process.
|
||||
|
||||
## Historical local regression evidence
|
||||
|
||||
- Shared, server, and UI typechecks: passed.
|
||||
- Focused UI/OpenAPI/server tests: passed.
|
||||
- Chat-channel PostgreSQL integration suite on fresh `chat_adapters_test_016`: 47/47 passed.
|
||||
- Deterministic browser suite `tests/e2e/chat-adapters-ui.spec.ts`: 4/4 passed.
|
||||
- Token gates and `git diff --check`: passed.
|
||||
|
||||
This evidence is useful for regression comparison, but it is incomplete release evidence. Identity linking was sufficient for the exercised runs, but the full permission-revocation and unlinked-participant governance matrix was not executed. Current live evidence now covers disabled-resource enforcement/recovery, one native outbound-file fixture, and a complete native question continuation. Broader modal behavior, file type/size rejection, rate limiting and ambiguous-send recovery, full App uninstall/reinstall, reconnect, and final cleanup assertions remain incomplete. Slack remains unqualified for stable release until the complete release-candidate runbook passes.
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# Microsoft Teams live qualification result — 2026-09-05
|
||||
|
||||
> **Status: blocked before provider setup; no live Teams scenario executed.** The branch contains substantial Teams hardening and local regression coverage, but none of it is real-provider evidence. This document is a blocker record, not a PASS.
|
||||
|
||||
## 2026-09-06 live-attempt checkpoint
|
||||
|
||||
Paperclip endpoint `00758007-1c59-45e9-bbef-3dc92c0fb20c` remains `draft` at `provider_setup`. Its connection has zero credential secret references, zero deliveries, zero conversations, and only the endpoint-creation audit row. At that historical checkpoint, the public messaging endpoint was reachable at:
|
||||
|
||||
`https://andy-constitutes-hockey-congressional.trycloudflare.com/api/chat-webhooks/2KMDqYFTcPXmEQVewVqmwMhBOnJyX7jJnzkjWOBNaqw/microsoft-teams`
|
||||
|
||||
An unauthenticated probe at that time returned the expected `409 chat_endpoint_runtime_unavailable` while the endpoint was draft. This proved public routing and fail-closed state handling for that temporary ingress, not Microsoft webhook authentication or a Teams round trip.
|
||||
|
||||
That hostname was an ephemeral development tunnel and is not a current callback or production ingress evidence. A later attempt must use the then-current callback and reconfigure Azure Bot if the tunnel has changed; stable release qualification requires a durable public origin.
|
||||
|
||||
The signed-in session is still the personal/free surface at `https://teams.live.com/v2/`. The exact external gates are:
|
||||
|
||||
- `https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade` — a Microsoft 365 work/school tenant identity allowed to register applications;
|
||||
- `https://portal.azure.com/#create/Microsoft.AzureBot` — Azure subscription/resource-group permission to create the single-tenant Azure Bot used by the documented manual path; and
|
||||
- `https://dev.teams.microsoft.com/apps` — custom-app upload permission, or tenant-admin publication/approval.
|
||||
|
||||
The live attempt did not progress far enough to observe a provider-side implementation defect. The blocker is the absence of a usable Microsoft 365 organization/tenant and its required provider permissions, not a Paperclip credential or webhook failure. Teams-focused local verification at this checkpoint passed 28 focused server tests, 11 shared credential-validation tests, and 12 fresh-database integration tests; those results remain local evidence only.
|
||||
|
||||
### Release decision at this checkpoint
|
||||
|
||||
Teams remains a release blocker for the five-provider claim. The signed-in account reaches Teams personal/free, but that identity cannot complete the organization-backed Entra application, Azure Bot, Teams Developer Portal, and custom-app installation path. No Teams credential, authenticated Bot Framework activity, conversation, task, publication, card action, or reconnect has been observed live. Deterministic and fresh-database coverage is valuable implementation evidence but cannot substitute for a Microsoft 365 work/school tenant and, where required, tenant-administrator approval. The earlier quick-tunnel route probe is not production ingress qualification.
|
||||
|
||||
## Production-readiness audit — 2026-09-06
|
||||
|
||||
The Teams connector is **not yet live-qualified or production-ready**. A code-level stress audit found and fixed additional defects:
|
||||
|
||||
- When direct-message reach was disabled, Paperclip filtered the turn before creating a task but had already persisted the full message and external principal. Admission now reads the current DM switch under the endpoint row lock and stores only a payload-free delivery envelope.
|
||||
- Outbound Teams files were passed to the pinned adapter in personal chats as though this were a native upload. The adapter only created a base64 data-URI activity attachment; it did not implement Microsoft's required file-consent card, accept invoke, provider-issued upload URL, upload, and file-information card sequence. Paperclip now publishes a safe task link for Teams attachments in every conversation surface instead of making that unsupported provider call.
|
||||
- Reach authorization was checked before the later issue/comment mutation, leaving a stale-admission window. The final task mutation now locks and revalidates the endpoint plus destination in one transaction for every provider. If DM, group, channel, repository, or chat reach was revoked first, Paperclip atomically stores only a payload-free filtered delivery, removes the event's otherwise-orphaned external principal, and creates no task or comment. Deterministic fresh-database races cover all three Teams reach controls and a Slack DM.
|
||||
- The pinned Teams adapter did not dispatch `messageUpdate` activities even though its public parser and the SDK expose the contract. Paperclip now supplements verified `editMessage` activities through that parser, deduplicates exact redelivery while preserving same-timestamp/different-body edits, persists the actor, and revalidates current principal authorization under lock before adding the lifecycle comment.
|
||||
- Opening a Slack or Teams form previously had a final authorization race after its first link check. Endpoint, destination, principal, and interaction authorization are now revalidated under the mutation lock immediately across provider modal opening, so revocation or demotion cannot race a stale modal into existence.
|
||||
- Public chat webhooks now use a dedicated 1 MiB raw-byte parser before the generic application parser. Declared oversize bodies fail before materialization, chunked bodies are stream-capped, content encoding fails closed, and exact raw bytes remain available for signature verification.
|
||||
- The pinned Teams adapter exposes its public `parseMessage` contract but does not dispatch Bot Framework `messageUpdate` activities. Paperclip now supplements the authenticated webhook path for the documented `messageUpdate` plus `channelData.eventType=editMessage` envelope, preserving the adapter's canonical thread and principal mapping. Concurrent duplicate callbacks collapse to one lifecycle row, while distinct edits with the same provider timestamp remain distinct through a content-bound revision key.
|
||||
- Opening a Teams question modal previously rechecked authority before resolving the form but not at the final provider-effect boundary. Paperclip now locks and revalidates the endpoint, destination, principal link, and Paperclip membership before opening the modal. A deterministic race proves that demotion from operator to viewer during the callback prevents the modal from opening.
|
||||
- Telegram and Teams edit lifecycle rows now retain the normalized external actor and perform the same locked principal authorization check before creating a Paperclip system comment. If an identity link or membership is revoked after webhook receipt, the late edit is filtered and its text is redacted from the durable delivery row.
|
||||
- Authenticated Teams `messageDelete`/`softDeleteMessage` and
|
||||
`messageUpdate`/`undeleteMessage` activities are now supplemented alongside
|
||||
edits. Exact callback duplicates collapse durably, edits cannot resurrect a
|
||||
deleted source message, and a later provider restoration reopens the
|
||||
lifecycle before subsequent edits. The advertised delete capability now
|
||||
matches this implementation.
|
||||
- GitHub App ids and Microsoft Bot application ids now have provider-global
|
||||
live ownership constraints independent of mutable owner or tenant metadata.
|
||||
Setup claims the identity before persisting newly supplied credentials, so
|
||||
concurrent cross-company setup has one winner and leaves no credentials on
|
||||
the loser without revealing the other company's endpoint or agent. If setup
|
||||
crashes after that claim, a later `configure` recovery must still match the
|
||||
claimed Bot application id; it cannot use the attention state to replace the
|
||||
immutable bot identity.
|
||||
|
||||
The final combined working tree passed 193/193 chat-channel integration tests on fresh migrated database `chat_adapters_test_final_20260906_1257`, 111/111 focused runtime/error/privacy tests, all package typechecks, token gates, and the deterministic five-provider browser suite. This remains local evidence only for Teams.
|
||||
|
||||
The setup wizard also now provides an exact Entra, Azure Bot, Teams Developer Portal, and Teams custom-upload field map plus a copyable Paperclip-specific manifest block. It explicitly distinguishes that block from a complete app package, so operators are not left to infer where each value belongs. At the time of this qualification record, the block omitted `webApplicationInfo` because Paperclip does not use Teams single sign-on. **September 7, 2026 correction:** Teams requires `webApplicationInfo` to bind the declared RSC permissions to the Entra app even without SSO. The current wizard includes that binding with a nonempty RSC-only resource; Paperclip still does not require registering an Entra Application ID URI or adding delegated Microsoft Graph permissions.
|
||||
|
||||
One remaining risk requires real-provider evidence before a production claim: private denial notices use Teams targeted messages. Microsoft moved this feature to general availability on July 30, 2026, although the pinned adapter README still calls it public preview. The local suite proves the adapter call contract, but the denial, removal-from-roster, and bounded-fallback paths still need real-provider validation.
|
||||
|
||||
The remaining live matrix is unchanged: installation, real webhook authentication, channel/root/reply ordering, DMs and group chats, reactions, Adaptive Card actions, identity linking, provider revocation, file receipt and publication, reconnect, retry, and cleanup have not run against Microsoft Teams. Paperclip endpoint removal archives the connection and retires its saved client secret; it does not delete the Entra registration or Azure Bot, remove the custom app package, or uninstall that app from teams and chats.
|
||||
|
||||
## Attempted environment
|
||||
|
||||
- Last pre-merge live-attempt source revision: `77ad5383e3a8badf7b1b0933a7e9c66469186d55`
|
||||
- Latest implementation revision covered by focused checks: `83018c688`
|
||||
- Teams FIFO, endpoint-generation fencing, per-thread and per-user service-URL egress, adapter compatibility, reach defaults, and pre-transport safety fixes are committed in the branch.
|
||||
- Provider session: Microsoft Teams personal/free at `teams.live.com`
|
||||
|
||||
No Microsoft client secret, access token, cookie, password, or one-time identity-link URL is recorded here.
|
||||
|
||||
## Blocker
|
||||
|
||||
The signed-in personal/free Teams account cannot access the organization-backed Teams Developer Portal, Entra registration, Azure Bot, and custom-app installation path required for the customer-owned bot. Navigating into that path reaches Microsoft's work-or-school organization gate. Live setup requires a Microsoft 365 work or school tenant with permission to create a single-tenant Entra application and Azure Bot, configure a custom Teams app, and install it or obtain tenant-administrator approval.
|
||||
|
||||
The run stopped before credential entry and before any provider webhook activity. A Microsoft 365 tenant login, and possibly tenant administrator approval, is required before live qualification can begin.
|
||||
|
||||
## Code qualification progress
|
||||
|
||||
The committed code serializes Teams turns in FIFO order and stores the latest Bot Framework `serviceUrl` as mutable route state per external conversation (plus per user for direct-message creation), outside the durable thread identity. Existing route-bearing thread IDs remain readable, while new IDs are canonical and route-free; a signed activity that arrives through a new regional route therefore continues the same Paperclip task. Every outbound operation uses an asynchronous context-local API client and the latest admitted route, so simultaneous conversations in different Microsoft regions cannot overwrite one another's route. The shipped setup is qualified only for Microsoft 365 commercial cloud tenants. Its defensive trust boundary accepts Microsoft-owned Connector host families or an exact explicitly configured API URL because signed activity carries the reply route; accepting a host is not sovereign-cloud qualification. Loopback, attacker-suffix, nonstandard-port, and wrong-path destinations fail before transport, and those local rejections are classified as definite failures rather than ambiguous `delivery_unknown` sends.
|
||||
|
||||
Additional hardening from this cycle is also local-only:
|
||||
|
||||
- Teams group chat reach now defaults to off. The change is delivered through forward-only migration `0244_tan_chat.sql`, so existing databases upgrade without rewriting migration history; group discovery no longer silently enables group reach.
|
||||
- A reply that arrives before its root remains retryable and ordered instead of creating a detached task or being acknowledged as complete too early.
|
||||
- Native file ingestion is limited to personal chats, where the bot file contract applies. Team channels and group chats retain bounded attachment metadata and provider links without invoking unsupported credentialed downloads.
|
||||
- Runtime callbacks are fenced to the endpoint credential generation, and pause, reconnect, rotation, and removal share a mutation lease so an old callback cannot mutate task state after endpoint state changes.
|
||||
- Stale or unauthorized Teams actions fail safely with a targeted payload-free notice; provider-retry duplicates cannot execute the action repeatedly.
|
||||
- The UI does not present misleading DM/group rows as independently discovered destinations when Teams reach is controlled by the connection-level access switches.
|
||||
|
||||
The pinned adapter contract now fails initialization if the internal API client or any wrapped outbound method drifts. Published-adapter tests directly exercise post, edit, reaction add/remove, delete, and concurrent cross-region `openDM`; a fresh-database integration test covers the terminal pre-transport failure and the Teams delivery reorder window. Focused runtime/classifier tests passed 54/54, the published-adapter/classifier subset passed 27/27, the fresh PostgreSQL Teams subset passed 9/9, and server typecheck passed. These results address ordering, regional isolation, SSRF exposure, and error classification in code, but remain local evidence until exercised through a real Microsoft 365 tenant.
|
||||
|
||||
On merge revision `da8f83d6c9befe7bf958f6d9cf12a95fc7e59e88`, the full chat-channel PostgreSQL integration suite passed 188/188 on fresh migrated database `chat_adapters_test_20260906_1140`; merge-conflict-focused server tests passed 355/355; and the deterministic browser suite `tests/e2e/chat-adapters-ui.spec.ts` passed 5/5 across Slack, GitHub, Teams, Discord, and Telegram. Implementation revision `83018c688` then passed the 42-test Discord adapter/runtime subset, the 34-test Discord/OpenAPI/UI contract subset, server/UI typechecks, token gates, a clean Discord patch application against the pristine package, and both working-tree checks. CI owns `pnpm-lock.yaml` and regenerates the PR lockfile artifact before its frozen install. This does not change the Microsoft 365 organization/tenant blocker or provide live Teams evidence.
|
||||
|
||||
## Qualification gap
|
||||
|
||||
All Teams live scenarios remain unexecuted: credential/setup verification, custom-app installation, team/channel discovery and enablement, channel root/reply boundaries, direct and group chats, identity linking and governance, Adaptive Cards/actions, native DM file handling and non-DM file fallback, post/edit publication behavior, edit/delete/restore receipt, duplicate delivery, permission or app revocation, reconnect/recovery, and cleanup. Deterministic local tests do not replace this missing real-provider evidence.
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
# Telegram live qualification result — 2026-09-05
|
||||
|
||||
For the later September 7 image/file handoff and real download checks, see
|
||||
[media qualification](2026-09-07-media-live-qualification.md).
|
||||
|
||||
> **Status: broad private-chat and group/topic live evidence, not full release qualification.** The latest live runs cover task controls, FIFO and burst handling, reactions, edits, native documents, task-generation races, the repaired interleaved status/final lane, group/topic isolation, removal/rejoin, the silent-publication boundary, and a complete native confirmation-to-continuation round trip. Broader media boundaries, global token revocation, and other runbook cases remain open.
|
||||
|
||||
## 2026-09-07 fresh-bot webhook-port and setup recovery
|
||||
|
||||
The user's new bot connection exposed a deployment/setup defect: the public
|
||||
Tailscale origin used port `10000`, which Telegram rejects. The connector now
|
||||
validates HTTPS and Telegram's allowed webhook ports **443, 80, 88, 8443** before
|
||||
provider access, credential writes, or reconnect lifecycle changes. Unsupported
|
||||
configuration returns the actionable `chat_telegram_webhook_url_unsupported`
|
||||
error without including the supplied URL or token. Regression coverage verifies
|
||||
both first setup and preservation of existing durable credentials on reconnect.
|
||||
|
||||
The canonical Telegram origin now uses public Tailscale Funnel **8443** forwarding
|
||||
only to the webhook-only proxy on `3104`. Public `10000` remains a compatibility
|
||||
route for already-configured Slack/GitHub callbacks; private tailnet `443` still
|
||||
serves the Board. Public Board/health/API requests return `404`. This supersedes
|
||||
the older temporary-tunnel topology below and the earlier private-8443 checkpoint.
|
||||
|
||||
The real **Reconnect bot** action reused the already-vaulted token successfully;
|
||||
the user did not need to create another bot or re-enter a secret. The verified
|
||||
bot is [MayaPaperclipQA1234bot](https://t.me/MayaPaperclipQA1234bot), endpoint
|
||||
`5b18b946-2b24-45b6-957f-783a0a735d8a`. Tapping **Start** discovered the account and
|
||||
displayed the native welcome without starting a failing agent run. The observed
|
||||
account was privately linked through Access, and **Continue setup** returned to
|
||||
the test step. Live inspection also found that **Open Telegram** incorrectly
|
||||
pointed back to BotFather; setup now projects the verified bot's own URL.
|
||||
|
||||
The linked private-chat request created `CHA-8`
|
||||
(`8feb3fca-5fdc-4659-bd99-e11c3f64d032`), conversation
|
||||
`0e63c3fb-026f-49c1-af40-383d42cb5cfd`. Run
|
||||
`e1e53d28-8d96-44ec-bf16-9ec5660ccca4` succeeded from
|
||||
`14:15:31.904Z` to `14:15:39.979Z`. Telegram visibly showed exact
|
||||
`TELEGRAM-LINKED-0907-OK`; the final published at `14:15:40.994Z`. Working and final
|
||||
each published in one attempt and share provider message `417200359:4`, proving
|
||||
an in-place update rather than two bot messages. The task remained `in_progress`
|
||||
and its conversation active; the company had no pending, retry, streaming, or
|
||||
ambiguous publication at the `14:20Z` audit.
|
||||
|
||||
The real **I've sent the test message** action completed setup on the restarted
|
||||
server and rendered **active**, with no stale **Continue setup** action. The
|
||||
Settings page was visually inspected. **Open Telegram** is now a native link
|
||||
with the verified bot URL rather than BotFather. Its href was verified in the
|
||||
live UI, but clicks did not create a tracked in-app-browser popup even after the
|
||||
native-link change; no visible blocker appeared. That host/external-link behavior
|
||||
remains unverified, and the working bot conversation is separate live evidence,
|
||||
not a claim that this popup opened successfully.
|
||||
|
||||
Source: `5bd9c0d55` plus the setup-edge repairs in this change, with the combined
|
||||
server restarted at `14:20:00Z`. Fresh-database chat integration passed **258/258**,
|
||||
recovery/status tests **135/135**, and focused UI tests **47/47**. This fresh-bot
|
||||
core journey does not rerun or upgrade every historical group/media/governance
|
||||
case below to the current source.
|
||||
|
||||
## 2026-09-06 merged-build tunnel-rotation retest
|
||||
|
||||
The live-tested merge commit is `da8f83d6c9befe7bf958f6d9cf12a95fc7e59e88`. After the account-less Cloudflare test tunnel expired, the bot webhook was rotated to the new verified URL with the already-vaulted token and webhook secret; neither credential was exposed. Telegram reported zero queued updates and no provider-side webhook error. A fresh `new` followed by a `task` command created one task and produced exact `TELEGRAM-MERGED-A-0906`. Its working placeholder and final share provider message ID `-1004415501660:69`, proving that the final edited the status in place. Both inbound command deliveries processed once and both publications completed with `attempts=1`, no error, and no pending, retry, failed, or ambiguous row. A plain unaddressed group follow-up was intentionally not delivered to the bot under Telegram privacy mode. Later implementation revision `83018c688` changes only Discord log redaction plus documentation and setup copy relative to that tested Telegram runtime.
|
||||
|
||||
A later group/topic iteration repeated the privacy and exact-publication path. Telegram delivered `/new` once, intentionally withheld the following plain unaddressed text under privacy mode, and then delivered the explicit `/task` command. Paperclip created only `CHA-90`, completed it successfully, and published working plus exact final `TELEGRAM-ITERATION-0906` with `attempts=1`, no errors, and the same provider message id `-1004415501660:74`. This is additional live evidence for command admission, privacy enforcement, and working-to-final in-place editing; it does not exercise the reconnect backlog repair below.
|
||||
|
||||
## 2026-09-06 reconnect backlog-preservation audit
|
||||
|
||||
The successful tunnel-rotation retest above had zero queued provider updates, so it did not exercise recovery of a backlog. A later code audit found that the reconnect path asked Telegram for `drop_pending_updates=true` whenever the public webhook URL changed. During a real ingress outage or domain migration, that option could silently discard messages Telegram had queued while Paperclip was unreachable. The clean exact response above remains valid positive transport evidence, but it cannot be cited as proof that queued updates survived a reconnect.
|
||||
|
||||
The working-tree repair now distinguishes first setup from recovery:
|
||||
|
||||
1. Initial bot setup may drop updates that predate the Paperclip connection.
|
||||
2. Every reconnect preserves pending updates, including a reconnect that changes the public webhook URL.
|
||||
3. Endpoint removal continues to delete the webhook without requesting a pending-update drop.
|
||||
|
||||
The shipped removal boundary also deletes Paperclip's registered command menu through the same durable maintenance outbox, then retires the saved token. It does not delete the BotFather bot or remove that bot from chats; those remain explicit provider-side cleanup steps.
|
||||
|
||||
The focused fresh-database regression passed 1/1, the adjacent reconnect subset passed 5/5, server typecheck passed, and formatting/diff checks passed.
|
||||
|
||||
The final working-tree retest then exercised the provider failure mode directly. Telegram updates `75` (`/new`) and `76` (`/task`) were sent while the prior quick-tunnel hostname was dead and therefore remained queued at Telegram. Paperclip restarted on the migrated current source, reconnected the bot to a fresh public origin, and preserved both pending updates. They arrived in provider sequence, processed once each with `attempts=1`, and created only `CHA-91`. The task reached `done`; its working state and exact final `TELEGRAM-FINAL-SOURCE-0906` each published once with no error and shared provider message id `-1004415501660:78`. Telegram Web visibly showed the exact final. This upgrades this specific backlog-preservation path from deterministic-only evidence to one live outage/rotation/replay pass; provider flood control, token revocation, and the rest of the failure matrix remain open.
|
||||
|
||||
As with Slack, the account-less Cloudflare quick tunnel was useful for finding and live-verifying the defect but is not production ingress. Stable qualification still requires a durable HTTPS origin and the remaining TG recovery cases on the final release-candidate source.
|
||||
|
||||
## 2026-09-06 final answer/recovery audit
|
||||
|
||||
The final transcript and durable-ledger review for `CHA-81` found one provider-visible exact final publication, `TELEGRAM-LATENCY7-Cobalt`, with `attempts=1`; Telegram also showed the native question card settled to **Answered: Cobalt**. No late duplicate or internal run summary reached the provider.
|
||||
|
||||
A third recovery run did execute after the answer continuation. Its comment stayed internal and the task then reached `done`, so the externally visible safety boundary held. The extra recovery incurred about $0.21 of model cost and is retained as efficiency evidence: it is the intentional productive-terminal fallback that prevents an `in_progress` task from being stranded, not a second answer publication. This observation does not upgrade Telegram to a complete runbook pass, and future tuning should preserve that liveness guarantee while avoiding unnecessary work when the continuation has already terminalized the task.
|
||||
|
||||
## 2026-09-06 current-build continuation closure
|
||||
|
||||
After the public test tunnel changed, Paperclip rotated the bot webhook to the current verified URL using the already-vaulted credential; no token was exposed. The first current-build request then exposed a real shared presentation defect: the exact final comment existed in Paperclip, but Telegram received only `Maya completed this turn.` because heartbeat materialized the final response as an internal comment.
|
||||
|
||||
The repaired path now authorizes only the selected final-assistant presentation of an exactly chat-bound run. A fresh request produced exact provider-visible `TG-CURRENT-BUILD-0906-C` instead of a generic completion. A fresh ordinary confirmation then rendered native **Approve** and **Reject** controls; selecting **Approve** edited the card to **Accepted**, scheduled one continuation, and produced exact provider-visible `TG-CONFIRM-CONTINUED-0906`. The final response appeared once, and no generic completion followed it. Raw reasoning, tool events, and internal logs remain in Paperclip.
|
||||
|
||||
Transcript review then found that the originating run's own meta-summary still appeared beside the native control and exposed internal interaction terminology. The final implementation keeps that source-run summary internal whenever its exact provider-visible interaction prompt exists, including when the user answers before presentation resolves. The native prompt and the later continuation remain external.
|
||||
|
||||
## 2026-09-06 native confirmation follow-up
|
||||
|
||||
Earlier provider checks on pre-merge revision `77ad5383e3a8badf7b1b0933a7e9c66469186d55` distinguished the native control from its downstream continuation:
|
||||
|
||||
- The older confirmation attempt exposed a link-only fallback gap and is not evidence for native Telegram actions.
|
||||
- A fresh confirmation on provider message `521…` displayed native **Yes** and **No** controls in Telegram. Selecting **Yes** was accepted exactly once, the sibling choice expired, and the same provider message was edited to **Accepted** with no buttons left active. Paperclip scheduled exactly one continuation.
|
||||
- The continuation run's final comment remained internal because its run lineage was not recognized as originating from the bound external turn. That older attempt exposed the defect. The current-build **Approve** retest documented above supersedes it and completed the native question-to-continuation round trip with exact final output.
|
||||
|
||||
## 2026-09-06 group and boundary extension
|
||||
|
||||
The live bot was installed in group `pc-e2e-telegram-0906`; the endpoint remained live through the following cases:
|
||||
|
||||
- **Captioned media defect and fix:** a 41-byte `text/plain` document initially normalized with zero attachments because the slash-command callback did not invoke the pinned Telegram adapter's `parseMessage`. The implementation now uses that parser for Telegram command captions. The live retry stored the durable attachment, the agent fetched it with HTTP 200, and Telegram received exact response `paperclip-live-telegram-media-proof-0906`.
|
||||
- **Topic isolation:** custom topic id `2` mapped to task `CHA-65` and native thread `telegram:-1004415501660:2`; General mapped separately to `CHA-66` and `telegram:-1004415501660`. No cross-topic task reuse was observed.
|
||||
- **Queue ordering:** A and B were sent six seconds apart. B was admitted only after A succeeded, the placeholder/final lane coalesced, and the exact final marker `tg-queue-A-then-B-0906` was visible. This is live FIFO evidence for one group conversation, not a universal throughput benchmark.
|
||||
- **Removal, rejoin, and migration:** `my_chat_member` plus the basic-group-to-supergroup migration marked the old resource unavailable and the new resource available once, while restoring the human group label. One stale legacy basic-group inventory artifact created before the fix remains in this disposable database; future migration and membership events use the corrected behavior. The artifact is historical local state, not a current provider failure.
|
||||
- **Silent publication boundary:** after the `03:44` restart, a prompt explicitly forbidding a public comment produced only generic provider text `Maya completed this turn.` Internal presentation comments are no longer auto-published. Explicit `allow_*` and runner-authored comments remain eligible. The unwanted auto-publication was an implementation defect and the live rerun verifies the fix.
|
||||
|
||||
## Scope
|
||||
|
||||
- Pre-merge source revision for the historical breadth checks below: `77ad5383e3a8badf7b1b0933a7e9c66469186d55`
|
||||
- Most recently live-rerun Telegram source revision: `da8f83d6c9befe7bf958f6d9cf12a95fc7e59e88`
|
||||
- Later implementation revision (Discord log redaction and documentation/setup-copy follow-up only): `83018c688`
|
||||
- Telegram provider-ordering, slash-command receipt, false internal-drain duplicate, stale-action denial, endpoint-generation fencing, command admission, provider-failure classification, coherent progress/status/final lane, native-confirmation lifecycle, and exact final-presentation lineage fixes are present in the final merge revision. The historical breadth checks exercised the pre-merge revision above; the merged-build section records the final live rerun.
|
||||
- Provider: Telegram, dedicated test bot in a private chat
|
||||
- Live checkpoint: 2026-09-05 through 2026-09-06
|
||||
|
||||
No bot token, webhook secret, cookie, password, or one-time identity-link URL is recorded here.
|
||||
|
||||
## Latest live breadth run
|
||||
|
||||
### Commands and linear task generations
|
||||
|
||||
The live private chat exercised `new`, `status`, and `close` as real Telegram commands. The recorded command deliveries used provider-native `chat_id:message_id` identities, processed with `attempts=1`, and had no redacted error. The task request after `new` returned the exact `telegram-command-prod-a74` response once. A later `status` reported the active task, and `close` closed the linear binding before the next generation.
|
||||
|
||||
Telegram commands continue to receive the normal provider receipt reaction because, unlike Slack slash callbacks, Telegram supplies a real message ID. Local regression coverage now asserts that this capability difference survives deferred delivery reconstruction.
|
||||
|
||||
### FIFO, bursts, reactions, and edits
|
||||
|
||||
The live ledger and provider UI showed:
|
||||
|
||||
1. The exact requests `tg-prod-fifo-one` and `tg-prod-fifo-two` were admitted once each and returned their matching final publications in provider order. Each final publication completed in one attempt with no error.
|
||||
2. A tighter same-second burst, `tg-prod-rapid-three` followed by `tg-prod-rapid-four`, produced two processed inbound deliveries and two one-attempt final publications in three-then-four order. The two wakes were allowed to coalesce operationally without merging, dropping, or reversing the externally visible results.
|
||||
3. Removing and then adding a reaction on provider message `417200359:143` produced one `reaction_removed` and one `reaction_added` delivery. Both processed once with no error.
|
||||
4. Editing a source message produced separately auditable `message_updated` deliveries for the provider message, without treating the edit as a duplicate of the original inbound event or starting an unintended replacement task.
|
||||
|
||||
### Native file proof
|
||||
|
||||
A Telegram document plus “Read the attached file and reply with exactly its Token value” produced one processed direct-message delivery, one stored Paperclip issue attachment, and the exact `chat-upload-a74` final response. Its working and final publications each completed in one attempt, and the final edited the working provider message in place. This proves the tested document path only; photos, audio, video, oversize files, malformed files, and download-failure recovery remain separate cases.
|
||||
|
||||
### Queued `new` generation race
|
||||
|
||||
The run deliberately put a slow task in one Telegram DM generation, sent another `new`, and then started a new task before the older task finished. The durable state shows distinct consecutive bindings (`CHA-54` and `CHA-55`) on the same Telegram chat. Both inbound requests processed once and both final publications succeeded once. The newer generation returned `telegram-new-generation-a74` before the older generation later returned `telegram-old-generation-a74`; neither final overwrote or attached to the other generation.
|
||||
|
||||
This is useful proof of generation isolation, not strict global FIFO across generations. Paperclip intentionally gives each task generation its own provider publication lane, so an older still-running task may finish after a newer one. The current run did not test cancellation of the old run, because `new` defines a new active binding rather than cancellation semantics.
|
||||
|
||||
### Delayed-status chronology defect found live
|
||||
|
||||
The sequence `new`, a delayed task request, then `status` exposed a provider-visible chronology problem. Status was sampled as `in_progress` and posted after the working placeholder, but the older final response later edited that earlier placeholder in place. Telegram therefore rendered the final answer above a now-stale-looking status message. Every transport operation succeeded, but the resulting conversation was not production-quality.
|
||||
|
||||
The final fix makes a task-bound status a durable `task_control` publication in the same conversation FIFO, re-samples authoritative task state at the outbox head, and treats the active run's provider message as one coherent lane. Status edits the open run's queued/working message; the final edits that same provider message again. Once terminal output exists, a later status has no open placeholder and posts separately instead of erasing the final.
|
||||
|
||||
The live final-revision rerun used `new`, then `Run sleep 12 then reply exactly tg-status-lane-6f13`, then `status` while `CHA-62` was active. Telegram showed the current `in_progress` state while the run was active and later showed only the final `tg-status-lane-6f13` in that bot-message position. There was no stale `Maya is working…` or `in_progress` sibling. The working, status, and final publication rows all share provider message ID `417200359:199`; each is `published`, `attempts=1`, with no error.
|
||||
|
||||
## Latest false-duplicate regression retest
|
||||
|
||||
On 2026-09-06 UTC, Telegram update `128` (`/new`) arrived at `04:26:26` and update `130` (the root request) arrived at `04:26:32`. Both deliveries processed with `attempts=1`, null errors, and no `duplicateCount` field, which represents zero duplicates. The exact final response `tg-no-false-duplicate` appeared promptly in Telegram.
|
||||
|
||||
Earlier delivery rows intentionally retain the false duplicate telemetry produced before the fix. They are preserved as bug evidence rather than rewritten to resemble the clean retest.
|
||||
|
||||
## Core-smoke result
|
||||
|
||||
The following private-chat behavior was observed on the recorded working tree:
|
||||
|
||||
1. Telegram delivered sequence `118` (`/new`) and sequence `119` (the next request) with the same second-resolution `sentAt` value.
|
||||
2. The corrected ordering uses Telegram's raw provider date together with monotonically increasing `message_id`, so Paperclip processed `/new` before the request even when their normalized timestamps tied.
|
||||
3. The corrected slash-command normalization preserved provider message ID `417200359:118`; the provider receipt reaction succeeded and the durable delivery's redacted error remained null. This supersedes the earlier sequence `114` run, where a synthetic hash was incorrectly passed to Telegram as a message ID and the receipt reaction failed.
|
||||
4. `/new` established the fresh boundary, the following request entered active issue `b2867d3e…`, and the provider showed the acknowledgement followed by the successful final response `tg-receipt-order-live`.
|
||||
5. The working and final publications each completed in one attempt with no error and reused provider message `417200359:121`, proving that the final response edited the working message in place instead of posting a duplicate.
|
||||
6. The focused same-second ordering regression passed before the live retest and now asserts the provider-native `chatId:messageId` shape.
|
||||
|
||||
This proof supersedes the previously observed same-second race. It does not by itself prove general burst handling across different tasks, multiple chats, or multiple workers.
|
||||
|
||||
## Pre-merge local regression evidence
|
||||
|
||||
- Telegram edit lifecycle rows now retain the normalized external actor and revalidate the current identity link and Paperclip membership under lock immediately before creating the lifecycle system comment. A deterministic race revokes the actor's link after the original message is admitted; the later edit is filtered, its text is removed from the durable row, and no task comment is created.
|
||||
- On that pre-merge working tree based on revision `77ad5383e`, the full chat-channel PostgreSQL integration suite passed 183/183 on fresh migrated database `chat_adapters_test_final_20260906_0833`.
|
||||
- Focused shared tests passed 11/11, focused server tests passed 194/194, and focused UI tests passed 41/41.
|
||||
- The deterministic browser suite `tests/e2e/chat-adapters-ui.spec.ts` passed 4/4, and shared, database, server, and UI typechecks all passed.
|
||||
- These deterministic checks support the live continuation fix but do not replace the remaining provider cases.
|
||||
|
||||
## Earlier core-smoke evidence
|
||||
|
||||
On the older `e5f3917b7` checkpoint, rapid updates `88` and `89` each produced one inbound delivery and one final publication in FIFO order. One Telegram Web client displayed an apparent duplicate, but an independent client, the provider event IDs, and Paperclip's durable records showed only one inbound event and one final publication. That older evidence remains a rendering-artifact diagnosis, not a substitute for the current run.
|
||||
|
||||
## Qualification gap
|
||||
|
||||
This was not a full Telegram runbook PASS. Private-chat commands, text documents, group privacy-mode operation, forum-topic boundaries, queue ordering, removal/rejoin, reaction add/remove, edits, and the silent-publication boundary now have live evidence, but the following still do not:
|
||||
|
||||
- disabled-resource enforcement and linked/unlinked identity governance;
|
||||
- forged and expired real-provider actions beyond the tested one-shot native confirmation; native rendering, continuation, sibling expiry, accepted-state edit, and exact final delivery now have live evidence;
|
||||
- audio, video, oversize or malformed media, and download-failure handling;
|
||||
- flood-control retry, global token revocation, recovery, and credential rotation; and
|
||||
- the complete cleanup and evidence checklist.
|
||||
|
||||
Telegram remains unqualified for stable release until the remaining live scenarios pass on the final release-candidate source.
|
||||
|
||||
September 7 evidence update: real photo receipt and return now have live proof in
|
||||
[the media qualification](2026-09-07-media-live-qualification.md) and
|
||||
[the native Codex/Luna qualification](2026-09-07-native-runner-chat-qualification.md).
|
||||
The native run inspected the provider-delivered image and returned the same
|
||||
bytes as a photo. This does not qualify audio, video, failure handling, or
|
||||
reuse of an older attachment outside the current wake.
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
# Discord live qualification result — 2026-09-06
|
||||
|
||||
For the reported missing-image failure and its successful September 7 retake,
|
||||
see [media qualification](2026-09-07-media-live-qualification.md).
|
||||
|
||||
For subsequent native Luna PNG+TXT returns and the latest deployment, use the
|
||||
[September 8–9 qualification ledger](2026-09-08-chat-queue-and-webhook-repair.md).
|
||||
The older gap list below is checkpoint-specific: files are no longer wholly
|
||||
untested, but remaining media boundaries and server 70's normalized-button
|
||||
denial still need live qualification. Gateway reconnection alone is not a
|
||||
provider conversation pass.
|
||||
|
||||
> **Status: core Discord transport, ordered follow-up bursts, receipt cleanup, and keep-open idle recovery have live proof, but the full DC1–DC7 matrix remains unqualified.** Paperclip has verified the dedicated bot identity, Message Content intent, Clawd membership, and a permission-complete text channel against Discord. The later clean-source checkpoint supersedes the intermediate unsolicited-recovery blocker.
|
||||
|
||||
## Resumed live setup — 2026-09-07 UTC
|
||||
|
||||
The operator entered the existing bot token directly into Paperclip's masked
|
||||
field; it was not reset, displayed, logged, or copied into this result. The
|
||||
first connection attempt reached Discord but failed with HTTP 400 / code 50035
|
||||
because Paperclip called the numeric Get Guild Member route with the literal
|
||||
`@me`. The scoped repair now uses the already verified Application ID as the
|
||||
bot user snowflake.
|
||||
|
||||
After that repair, the preserved provider token connected successfully on the
|
||||
working tree based on `f5f31d2e1`. The Paperclip endpoint reached its real
|
||||
`verifying` state with bot external ID `1546330979860221952` and provider
|
||||
account/server ID `1457808928258658549`; the UI advanced to **Try Maya E2E in
|
||||
Discord**. This provider-backed transition proves that the token identifies the
|
||||
configured Application ID, Message Content intent is enabled, the bot is a
|
||||
member of Clawd, and at least one text channel grants the complete required
|
||||
permission set. The scoped fix and this result must be committed and its
|
||||
automated checks recorded before treating the revision as a release candidate.
|
||||
|
||||
This was partial DC1 setup evidence at the time. The linked conversation pass
|
||||
below supersedes that limitation, while DC2–DC7 and the unexercised DC1 cases
|
||||
remain open.
|
||||
|
||||
## First native root checkpoint — 2026-09-07 UTC
|
||||
|
||||
In Clawd channel `1457808933082108089`, a real root mention produced the `eyes`
|
||||
receipt, exactly one native public thread (`1546509943639773244`), exactly one
|
||||
Paperclip task (`CHA-3`, issue `1976d84b-0bdf-4342-8afa-1a3e5d9be57c`), and one
|
||||
bound conversation (`123b687c-96d7-4164-bf58-bc95edf2bc8c`). Because the
|
||||
Eigenjoy Discord principal was unlinked at admission, the turn correctly
|
||||
published the safe low-trust-isolation refusal in that thread instead of agent
|
||||
output. The operator then completed the private identity link to the local
|
||||
Paperclip board; no one-time link or credential is recorded here. A fresh root
|
||||
must still prove the linked path because linking cannot retroactively change
|
||||
the trust boundary of the already admitted guest turn.
|
||||
|
||||
The live refusal also exposed a receipt-lifecycle defect: the `eyes` reaction
|
||||
remained on the root after the visible terminal failure. The implementation
|
||||
had a durable add-only action and never invoked the adapter's idempotent
|
||||
reaction removal, despite DC4 requiring both add and remove. The scoped repair
|
||||
stages a Discord-only removal in the same transaction that records the causal
|
||||
terminal publication, then attempts it under the same credential lease; a
|
||||
crash or transient provider error resumes from the durable action without
|
||||
replaying the terminal message. It removes the working receipt rather than
|
||||
replacing it with a success or failure emoji. The fresh linked turn below
|
||||
verified that the receipt is now cleared after the terminal reply.
|
||||
|
||||
## Successful linked round trip — 2026-09-07 UTC
|
||||
|
||||
The live source was the dirty working tree based on `1325329e3`, started at
|
||||
13:44:55 UTC; this evidence must therefore be repeated on the final clean
|
||||
release-candidate SHA before release. In Clawd `#general`, the linked Eigenjoy
|
||||
principal created native thread `1546513811672932372`, exactly one Paperclip
|
||||
task (`CHA-4`, issue `c65f32f8-a612-4f85-97c5-61bed2de58e2`), and one bound
|
||||
conversation. Only `#general` was enabled in Paperclip; the other ten discovered
|
||||
channels were disabled.
|
||||
|
||||
An unmentioned follow-up (`1546516684129575123`) in that native thread asked
|
||||
for the exact text `DISCORD-LIVE-0907-ROUNDTRIP-OK`. Run
|
||||
`2443fa37-ea1e-436b-a1af-3ad6e58afc51` ran from 13:45:11 to 13:45:17 UTC and
|
||||
succeeded. Discord reply `1546516692031504485` contained the exact marker, and
|
||||
the working receipt was cleared. This proves a real linked root boundary,
|
||||
native thread reuse for an unmentioned follow-up, task/run execution, exact
|
||||
final presentation, and terminal receipt cleanup through the configured bot.
|
||||
|
||||
The endpoint `af23c9d0-8d7f-495c-a45c-ba9ab1ee9686` was active and setup-complete
|
||||
in the UI. After the successful reply, however, generic task recovery spawned
|
||||
an unsolicited additional run (`6a3e0303-b5f2-4e32-8e36-790a892f07b6`). That is
|
||||
not acceptable production behavior: a completed Discord turn must not trigger
|
||||
new agent work without a new admitted user event. The recovery fix and a clean
|
||||
live rerun are still required. No provider credential, identity-link secret, or
|
||||
private callback value is recorded here.
|
||||
|
||||
### Rapid follow-up burst checkpoint
|
||||
|
||||
A later three-message burst on the same `CHA-4` Discord thread persisted all
|
||||
three inbound messages in order. The first message started run
|
||||
`4e42c03d-2c80-45ed-bb99-84a5b7c94c02`; the second and third messages were
|
||||
coalesced into one deferred wake and then run
|
||||
`2ae3cb6e-b33c-4c93-a029-02916210d142`. The provider-visible result was two
|
||||
replies for the three inputs: an initial `ALPHA` acknowledgement, followed by
|
||||
the combined exact `ALPHA BETA` result. The first turn took approximately 82.6
|
||||
seconds and the second approximately 50.1 seconds, so this is ordered-delivery
|
||||
and coalescing evidence, not an instant-response claim. All three working
|
||||
receipts were cleared when their causal runs reached terminal publication.
|
||||
|
||||
The receipt-retirement audit confirms why the coalesced case is lossless:
|
||||
deferred wake merging preserves the ordered `wakeCommentIds` set, promotion
|
||||
copies that set to the successor run, and terminal Discord publication selects
|
||||
receipt actions for every exact linked inbound comment in that run. It does not
|
||||
clear unrelated or later thread receipts. No additional automatic recovery run
|
||||
was present in the 13:58 UTC check, but `CHA-4` had been marked done by then;
|
||||
that observation does not independently prove the new in-progress recovery
|
||||
guard.
|
||||
|
||||
### Clean keep-open recovery qualification — 2026-09-07, 13:59 UTC
|
||||
|
||||
This checkpoint supersedes the pending keep-open retest and the earlier
|
||||
unsolicited-recovery blocker. On clean source revision `5bd9c0d55`, an
|
||||
unmentioned follow-up in native thread `1546513811672932372` left CHA-4
|
||||
deliberately `in_progress` and requested exactly `DISCORD-IDLE-WAIT-OK`. Run
|
||||
`f8c9dbe2-7e94-469d-8345-717eb7dad1bf` ran from `13:59:31.087Z` through
|
||||
`13:59:38.234Z` and succeeded. Discord
|
||||
[reply 1546520298793468036](https://discord.com/channels/1457808928258658549/1546513811672932372/1546520298793468036)
|
||||
contained exactly that marker.
|
||||
|
||||
CHA-4 remained `in_progress` with its conversation active for more than eight
|
||||
minutes after the terminal reply, with no additional run. This proves the
|
||||
repaired idle-chat boundary live: an open conversation waits for new provider
|
||||
input instead of being reclassified as stranded work.
|
||||
|
||||
After the latest setup-edge changes, the full chat integration suite passed
|
||||
**258/258** and the combined process-recovery/status-payload suite passed
|
||||
**135/135**, both with zero skips. The deterministic browser suite had passed
|
||||
**5/5** on clean revision `5bd9c0d55`, but has not yet been rerun after the
|
||||
latest setup-edge/UI changes; the current working tree is therefore not being
|
||||
claimed browser-green here.
|
||||
|
||||
## Historical live-attempt checkpoint — superseded above
|
||||
|
||||
The authorized provider target is the `Clawd` Discord server, numeric ID `1457808928258658549`, using the user's Eigenjoy account. The latest in-app-browser attempt reached Discord's login/QR flow in both the Developer Portal and server tabs. It did not reach application creation or expose a bot token. Login completion is therefore the current external gate.
|
||||
|
||||
### Release decision at this checkpoint
|
||||
|
||||
At this historical checkpoint, Discord remained blocked before provider setup.
|
||||
The resumed setup evidence above supersedes that gate while preserving this
|
||||
record of what had not yet been tested.
|
||||
|
||||
Once the authenticated session is available, the required path is:
|
||||
|
||||
1. create a dedicated Discord application and bot for the immutable Paperclip agent;
|
||||
2. enable Message Content Intent and enter only Application ID, Server ID, and the write-only bot token in Paperclip;
|
||||
3. inspect the generated OAuth URL for exactly the `bot` scope and permission integer `309237763136`, with the Clawd server pinned and server selection disabled;
|
||||
4. install the bot in Clawd, connect it in Paperclip, enable only the intended test channel, and execute DC1–DC7 from the browser runbook.
|
||||
|
||||
There is no managed bot-provisioning path, public webhook URL, interactions public key, slash command, or endpoint delivery choice in the current product.
|
||||
|
||||
No bot token, cookie, password, MFA value, or one-time identity-link URL is recorded here.
|
||||
|
||||
## Implemented behavior and remaining live proof
|
||||
|
||||
The current native Discord implementation includes:
|
||||
|
||||
- a long-lived Gateway runtime with bounded reconnect/retry behavior and full provider `retry_after` waits rather than an application-level 60-second cap;
|
||||
- immutable application identity, including a database uniqueness constraint that prevents one Discord Application ID from backing multiple active Paperclip agent endpoints even across different servers;
|
||||
- server and effective-channel-permission verification, channel discovery, a Paperclip allowlist, and a separate direct-message reach switch;
|
||||
- one root mention to one Discord public thread and one Paperclip task, with thread replies serialized onto that task and DMs isolated into linear task generations;
|
||||
- endpoint, resource, principal, and root-message preflight before provider-thread creation; denied roots retain only a payload-redacted filtered audit and create no provider thread or Paperclip work;
|
||||
- crash-safe root activation: an allowed root persists a provisional receipt before the provider POST, then recovery idempotently creates or reuses the thread and treats Discord error `160004` as an existing-thread reconciliation;
|
||||
- explicit missing-root filtering plus retryable ambiguous transport and authentication failures, so uncertainty is neither silently discarded nor misreported as a completed binding;
|
||||
- durable message links, endpoint-generation fencing, reaction hydration, edit/delete lifecycle handling, embeds/buttons, and bounded Discord-CDN attachment ingestion;
|
||||
- a fail-fast compatibility marker and required-method contract for the pinned SDK patch;
|
||||
- 25-second REST deadlines and structured preservation of Discord 401, 403, 404, 429, and `retry_after` failures without copying raw provider bodies, user content, credentials, interaction tokens, or derived thread names into exceptions or logs; and
|
||||
- the shared safe-publication, ambiguous-delivery, identity, permission, audit, and internal-content boundaries used by the other providers.
|
||||
|
||||
The linked run above now demonstrates the primary root, thread-reuse, exact
|
||||
final-response, and receipt-cleanup path. The remaining items are still
|
||||
code-level claims until the corresponding DC cases exercise them against the
|
||||
real provider.
|
||||
|
||||
## Historical code-audit status before the linked live run
|
||||
|
||||
The final hardening removed the code-level release blockers found in the root-activation and lifecycle audit: denied roots no longer create an inert provider thread; a crash between Discord thread creation and Paperclip binding now resumes through the persisted provisional receipt and idempotent reconciliation; provider response bodies and callback errors no longer disclose content or credentials through diagnostics; retry scheduling honors long Discord backoff windows; reconnect now has a distinct, payload-redacted activity action; and Discord `50001`/`50013` destination permission failures disable only the affected resource rather than putting the whole endpoint into attention. True token/app authentication failures and unrelated authorization errors remain endpoint-wide. The compatibility marker, required patched-method checks, clean patch application against the pristine package, and 25-second REST boundary make SDK drift and stalled provider calls fail visibly rather than weakening those guarantees. Per repository policy, CI owns `pnpm-lock.yaml`; its PR workflow regenerates a lockfile artifact from the manifests before running the frozen install.
|
||||
|
||||
At that checkpoint, no code-audit blocker was recorded and none of the behavior
|
||||
had yet been observed against the real provider account/server. The linked live
|
||||
run above supersedes the latter statement and exposed the unsolicited recovery
|
||||
run as a current blocker. Live proof must still cover denied-root silence,
|
||||
provisional recovery, existing-thread reconciliation, files/interactions,
|
||||
Gateway reconnect, rate limits, token rotation, and the visible management
|
||||
surfaces. The adapter patch remains version-sensitive; any dependency update
|
||||
requires the compatibility and provider contracts to rerun.
|
||||
|
||||
## Local regression evidence
|
||||
|
||||
- Final Discord implementation revision: `83018c688` (log-redaction hardening); parent merge revision: `da8f83d6c9befe7bf958f6d9cf12a95fc7e59e88`.
|
||||
- Before the final merge, Discord-focused adapter/runtime tests passed 41/41.
|
||||
- Before the final merge, fresh PostgreSQL Discord integration tests passed 2/2, including concurrent identity claims.
|
||||
- All migrations and migration-safety checks passed, including global Discord Application ID uniqueness.
|
||||
- On the parent merge, the full chat-channel PostgreSQL integration suite passed 188/188 on a fresh migrated database, merge-conflict-focused server tests passed 355/355, and the deterministic five-provider browser suite passed 5/5.
|
||||
- On the Discord implementation revision, the 42-test Discord adapter/runtime subset and 34-test Discord/OpenAPI/UI contract subset passed, along with server/UI typechecks, token gates, and both working-tree checks.
|
||||
- The Discord patch applied cleanly to a pristine `@chat-adapter/discord@4.39.0` package, and the patched distribution passed syntax and compatibility checks. CI will regenerate the PR lockfile artifact before its frozen install, as required by repository policy.
|
||||
- The post-audit Discord adapter/runtime subset passed 48/48, including raw-provider-body and callback-error redaction plus a 120-second `retry_after` contract; the focused reconnect/removal PostgreSQL scenario also passed and proved secret replacement, old-secret retirement, runtime replacement, identity/history/access preservation, redacted reconnect activity, and final Paperclip credential cleanup. Endpoint removal does not uninstall the bot from the Discord server or delete its Developer Portal application; those remain separate provider-side cleanup steps.
|
||||
- The final Discord permission classifier/adapter subset passed 49/49, and its database-backed publication regression proved that `50013` cancels only the affected publication/resource while the endpoint remains active. The final combined working tree then passed 193/193 chat-channel integration tests on fresh migrated database `chat_adapters_test_final_20260906_1257`, 111/111 focused runtime/error/privacy tests, all package typechecks, token gates, and the deterministic five-provider browser suite.
|
||||
|
||||
This evidence supports implementation integrity. Provider installation,
|
||||
Message Content intent, effective `#general` permission, a native root/thread,
|
||||
linked identity, exact final reply, and receipt cleanup now also have live
|
||||
proof. It does not replace the remaining Gateway-reconnect, rate-limit,
|
||||
restart, file, action, negative-reach, token-rotation, and cleanup cases.
|
||||
|
||||
## Qualification gap at the September 7 checkpoint
|
||||
|
||||
Provider credential validation, Message Content intent, Clawd membership,
|
||||
`#general` enablement, root-thread creation, a linked unmentioned follow-up,
|
||||
exact final presentation, and working-receipt removal now have live proof. The
|
||||
unsolicited post-completion recovery run was repaired, and the clean keep-open
|
||||
checkpoint above proves the fix against the real provider.
|
||||
The live three-message burst now proves ordered persistence, deferred coalescing,
|
||||
two causal runs, combined final presentation, and cleanup of every causal
|
||||
receipt, with the observed 82.6-second and 50.1-second turn latency recorded
|
||||
above. Disabled-channel silence, denied-user behavior, provisional recovery,
|
||||
existing-thread reconciliation, duplicate/reconnect fencing, edits/deletes,
|
||||
embeds/actions, inbound/outbound files, DMs, ambiguous sends, token rotation,
|
||||
intent revocation, provider links, management surfaces, and cleanup remain
|
||||
open. Discord remains unqualified for stable release until the remaining DC
|
||||
cases pass on one final clean release-candidate SHA.
|
||||
|
|
@ -0,0 +1,734 @@
|
|||
# Chat adapters live qualification addendum — 2026-09-06
|
||||
|
||||
This addendum records the qualification state observed on 2026-09-06. It is
|
||||
deliberately narrower than the provider runbooks: automated proof and live
|
||||
provider proof are reported separately, and an account page being reachable is
|
||||
not counted as a successful end-to-end conversation.
|
||||
|
||||
## Reliability work completed in this pass
|
||||
|
||||
- Provider-visible mutations are fenced against credential rotation, pause,
|
||||
reconnect, and removal with durable credential leases and generation/ref
|
||||
checks.
|
||||
- Outbound sends use short durable claims around provider I/O. A response lost
|
||||
after provider acceptance is quarantined as `delivery_unknown`; it is not
|
||||
replayed automatically.
|
||||
- Explicit duplicate-risk retries are audited and single-owner. Slack
|
||||
slash-command roots persist a provider-confirmed phase before the separate
|
||||
Paperclip task admission phase, so crash recovery cannot post a second root.
|
||||
- Slack slash-command authorization and destination reach are snapshotted in a
|
||||
transaction that releases its row locks before provider I/O. That snapshot
|
||||
authorizes only the Slack root send. The later Paperclip task admission is a
|
||||
separate mutation that rechecks current endpoint reach, resource state,
|
||||
identity link, membership, and guest sponsorship after any crash or restart.
|
||||
Reclaimed admission workers carry a durable ownership token so an obsolete
|
||||
worker cannot settle the successor's attempt. A recovered command cannot
|
||||
reactivate a disabled setup destination, including when its durable envelope
|
||||
was written by an older version. Rejected, unapplied deliveries retain only
|
||||
identifiers needed for deduplication and filtering diagnostics, not message
|
||||
text or principal profiles.
|
||||
- Receipt reactions use their own idempotent outbox. A Slack retry that reports
|
||||
`already_reacted` settles successfully, while rate limits retain their full
|
||||
provider retry interval.
|
||||
- Inbound turns are processed in durable provider order under a renewable
|
||||
conversation lease. Lifecycle changes and credential changes fence stale
|
||||
runtimes instead of allowing them to commit later work.
|
||||
- Run completion waits for the runner's presentation decision and suppresses a
|
||||
generic completion when an explicitly authorized final response exists. A
|
||||
provisional same-run final comment can be upgraded to the externally visible
|
||||
response without creating a duplicate comment.
|
||||
- GitHub verifies webhook signatures and current installation/repository reach
|
||||
before retaining a bounded recovery payload. Durable claims survive process
|
||||
restarts, fence credential changes, and redact terminal payloads. A manual
|
||||
provider redelivery can rearm a terminal failure only for the identical event
|
||||
and body digest; lifetime attempt ownership is not reset. Both GitHub mention
|
||||
forms work, while setup instructions show the App's bare slug.
|
||||
- Discord responses exceeding the provider's rendered message limit are sent
|
||||
losslessly as a Markdown attachment. Only the safe external response is used;
|
||||
internal reasoning and logs are not included.
|
||||
- Telegram can finish an already-queued second turn after natural task
|
||||
completion, but cannot cross an explicit `/new` or `/close` boundary. Teams
|
||||
thread decoding validates canonical encoding before interpreting legacy IDs.
|
||||
- Invalid publication payloads fail individually instead of poisoning the
|
||||
global queue. Transient preparation failures use bounded backoff, and the
|
||||
same drain can continue to a healthy publication behind the failed row.
|
||||
- Provider-confirmed Slack admissions on paused or attention endpoints remain
|
||||
parked without occupying the active worker page. They become eligible again
|
||||
after the endpoint is repaired or resumed; active endpoints can keep moving.
|
||||
- Dual-purpose connectors keep their chat setup separate from tool credentials.
|
||||
The tool connection flow excludes chat-only methods from selection,
|
||||
recommendations, and submission, and agent-facing connection intents expose
|
||||
only tool methods. GitHub's personal-token fallback therefore does not ask
|
||||
for chat App credentials or strand the user on another chooser. A tool-access
|
||||
request for a chat-only provider is rejected.
|
||||
|
||||
## Automated checkpoint
|
||||
|
||||
- Full chat integration suite: 240/240 passed on a newly created PostgreSQL
|
||||
database both before and after merging `origin/master` at `856813ba3`.
|
||||
The post-merge database is `chat_adapters_test_20260906_full2480`; the run
|
||||
includes all five provider fixtures. Provider transport is simulated.
|
||||
- Focused server/API/UI checks: 247/247 passed across 21 files. Post-merge safe
|
||||
publication/projection checks also passed 22/22.
|
||||
- The upstream runner slice passed 85/85. Tool-setup/catalog/shared-definition
|
||||
regression checks passed 127/127 (106 UI and 21 shared assertions).
|
||||
- The connection-intent service suite passed 8/8, with all seven
|
||||
embedded-PostgreSQL cases executed rather than skipped.
|
||||
- Deterministic chat-adapter browser checks: 5/5 passed after the merge.
|
||||
Provider API responses are mocked, so this is UI regression evidence only.
|
||||
- Direct shared, server, and UI TypeScript checks passed after the merge.
|
||||
- The post-merge UI production build passed, with existing CSS/font and
|
||||
chunk-size warnings.
|
||||
- `git diff --check` and UI token gates passed. The lockfile is the exact
|
||||
upstream CI-owned artifact; no hand-authored lockfile changes are included.
|
||||
- Earlier full-suite hangs were traced to synthetic 90-second test leases left
|
||||
behind by fault-injection cases; those fixtures now clean up only after
|
||||
verifying the ownership fence. Another run was interrupted by macOS sleep.
|
||||
The passing full run kept the machine awake for the test process and used no
|
||||
temporary diagnostic instrumentation.
|
||||
- The repository-wide `pnpm test:run` previously failed on unrelated runtime
|
||||
and test-harness issues. Repository-wide tests, typecheck, and build are not
|
||||
claimed green; the evidence here is the named focused verification.
|
||||
|
||||
## Live provider evidence and remaining gates
|
||||
|
||||
### Slack
|
||||
|
||||
- The existing Slack app is `maya-paperclip` (`A0C0NSMSA5N`).
|
||||
- A historical native-question thread was visually inspected. The question was
|
||||
answered, but the visible terminal reply was the generic “Maya completed this
|
||||
turn.” This is a real quality failure, not a successful qualification.
|
||||
- That historical fixture lived in a temporary database that no longer exists,
|
||||
so its comment/run/publication provenance cannot be reconstructed honestly.
|
||||
- The persistent isolated Paperclip instance on port 3103 currently has a fresh
|
||||
draft endpoint and no conversations or activity. It therefore provides no
|
||||
fresh Slack round-trip proof yet.
|
||||
- Slack's **Show** control for the Signing Secret did not respond after the
|
||||
documented fresh-tab retry. The Mac session then locked. A fresh round trip
|
||||
still requires the signed-in operator to reveal/copy that existing app secret
|
||||
(or rotate it deliberately), reconnect the draft, and send a new native
|
||||
question through completion. The new run must verify the exact final text,
|
||||
reaction behavior, one-thread/one-task binding, audit rows, and absence of
|
||||
duplicate provider messages.
|
||||
|
||||
### GitHub
|
||||
|
||||
- A GitHub App named `Paperclip Maya E2E 0906` was created with App ID `4853886`.
|
||||
- It is not installed, its private key has not been generated, and the webhook
|
||||
save against the temporary public callback was blocked by the browser tool's
|
||||
external-write review. The signed-in GitHub confirmation had already been
|
||||
completed; this was not a provider login or MFA gate. No issue/PR comment
|
||||
round trip has therefore been qualified.
|
||||
|
||||
### Discord
|
||||
|
||||
- The intended target remains the `Clawd` server (`1457808928258658549`) and
|
||||
channel `1457808933082108089`.
|
||||
- The saved account password was rejected before the provider MFA step, so a
|
||||
Discord application/bot was not created or installed. There is no live
|
||||
Discord message proof yet.
|
||||
|
||||
### Microsoft Teams
|
||||
|
||||
- The available login reaches personal Teams, but no Microsoft 365 tenant/admin
|
||||
context is available for Bot Framework registration, consent, packaging, and
|
||||
installation. Personal Teams login is not evidence that the Teams adapter
|
||||
works.
|
||||
|
||||
### Telegram
|
||||
|
||||
- Telegram login/QR access was completed earlier, but no fresh bot endpoint and
|
||||
complete message/reaction/attachment round trip was recorded against the
|
||||
persistent 3103 fixture in this pass. Telegram remains unqualified live.
|
||||
|
||||
## Release interpretation
|
||||
|
||||
The hardening and automated checks materially improve crash recovery, ordering,
|
||||
credential fencing, and auditability, but live qualification is not complete.
|
||||
Do not describe any of the five providers as production-qualified until a fresh
|
||||
provider event reaches the persistent isolated instance and its provider UI,
|
||||
Paperclip task/comment/run, outbox state, reactions/actions, and terminal reply
|
||||
have all been checked together.
|
||||
|
||||
## Resumed qualification — 2026-09-07 UTC
|
||||
|
||||
This checkpoint supersedes the setup gates above without changing the historical
|
||||
observations or claiming a completed provider conversation.
|
||||
|
||||
### GitHub
|
||||
|
||||
- The App now has two registered private-key fingerprints. Neither private PEM
|
||||
was available in the local Downloads directory, and GitHub's settings page
|
||||
offered no download for the registered keys. No replacement key was generated
|
||||
or existing key deleted by the agent in this resumed pass. The operator must
|
||||
recover the original browser download or deliberately generate and retain a
|
||||
replacement; the PEM must stay out of chat and logs.
|
||||
- The old temporary callback hostname no longer resolved. GitHub's delivery
|
||||
detail explicitly reported a failure to connect to the host. The webhook-only
|
||||
tunnel was replaced, the App callback was updated, and the setup ping was
|
||||
redelivered once. Paperclip verified its signature at
|
||||
`2026-09-07T01:33:42.242Z`. Delivery ID:
|
||||
`193f08a6-aa5b-11f1-8d07-d6d11e41dcde`.
|
||||
- The public tunnel forwards only provider webhook POSTs; a public request to
|
||||
`/api/health` returned 404. The local-trusted board API was not exposed.
|
||||
- The provider UI was checked directly: Issues and Pull requests are read/write,
|
||||
Metadata is read-only, and only Issue comment and Pull request review comment
|
||||
are selected. GitHub's automatic installation events need no checkbox.
|
||||
A new integration regression accepts `/app.events` containing only the two
|
||||
selectable events.
|
||||
- The App remains uninstalled. A signed ping proves webhook delivery and
|
||||
signature verification only, not repository reach or an issue/PR round trip.
|
||||
|
||||
#### GitHub live checkpoint — 2026-09-07 13:00 UTC
|
||||
|
||||
This later checkpoint supersedes the uninstalled/no-private-key state above.
|
||||
The operator authorized a newly downloaded private key, and it was imported
|
||||
through Paperclip's masked file control without reading, displaying, or
|
||||
recording its contents. Paperclip verified App `4853886`, discovered the single
|
||||
installation `159668881`, and reconciled exactly the two approved private test
|
||||
repositories.
|
||||
|
||||
The first real setup issue is
|
||||
[`cryppadotta/paperclip-chat-e2e-enabled#1`](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/issues/1).
|
||||
Root comment `5570993571` produced exactly one Paperclip task, `CHA-1`
|
||||
(`07a57128-20ef-4905-aa85-3bbcb4f2769e`), and one external conversation
|
||||
(`6a6d6bfa-4b21-45d7-87b3-9a8885449c5a`). GitHub displayed one eyes reaction
|
||||
and bot reply `5570994445`. The reply correctly failed closed because the turn
|
||||
belonged to an unlinked external guest and isolated guest execution was not
|
||||
available. This proves signed issue-comment ingress, repository admission,
|
||||
one-issue/one-task binding, reaction delivery, and safe containment; it does not
|
||||
prove a successful agent response.
|
||||
|
||||
The endpoint remains `verifying`. Paperclip opened the private confirmation
|
||||
flow for `cryppadotta` to the signed-in board account, but the user-controlled
|
||||
identity confirmation is still pending. No confirmation URL or token was
|
||||
recorded. The retained `CHA-1` task remains low-trust; after confirmation, a
|
||||
fresh GitHub issue is required to qualify the linked path and the unmentioned
|
||||
follow-up response.
|
||||
|
||||
The current Cloudflare webhook-only receiver remains in service for this test.
|
||||
The host's Tailscale connection is healthy, but Funnel is disabled for the
|
||||
tailnet and awaits administrator enablement before it can replace that receiver.
|
||||
The GitHub App homepage still points to the earlier temporary public host; that
|
||||
is a minor setup-polish defect, while the signed webhook callback itself remains
|
||||
the operative ingress route.
|
||||
|
||||
### Discord
|
||||
|
||||
- The user completed App creation. `Paperclip Maya E2E` now exists under
|
||||
`eigenjoy` with App ID `1546330979860221952`, and its Bot settings are reachable.
|
||||
- Paperclip's draft has that Application ID and the requested Clawd server ID.
|
||||
The generated bot-only installation link locks the server selection to
|
||||
`1457808928258658549`; no unrelated server is targeted.
|
||||
- The installation flow requires a separate main-Discord login despite the
|
||||
Developer Portal session. That login is open for the operator. Message Content
|
||||
Intent, deliberate token generation, and server installation still require
|
||||
completion. No native Discord message has been qualified in this pass.
|
||||
|
||||
### Telegram credential incident and containment
|
||||
|
||||
- The signed-in Telegram browser reached the official BotFather conversation
|
||||
for the existing test bot `@MayaPaperclipQA0905Bot`.
|
||||
- The agent incorrectly copied a message's concatenated DOM text, appending two
|
||||
timestamp digits to the token. Paperclip rejected the resulting setup request.
|
||||
The HTTP failure logger then recorded the raw submitted credential object.
|
||||
This was both an agent copy error and a real product credential-redaction bug.
|
||||
- The isolated live server was stopped, the form and in-memory copied value
|
||||
cleared, and the credential object removed from the local test log. A
|
||||
metadata-only scan of the relevant local logs found no remaining raw
|
||||
credential objects or Telegram-token-shaped strings. This local cleanup does
|
||||
not revoke the token or erase previously emitted diagnostic output.
|
||||
- The affected bot token must be rotated in BotFather before further live use.
|
||||
No new token should be sent through chat or printed during qualification.
|
||||
- The fix redacts whole credential envelopes plus provider-specific camel/snake
|
||||
case fields. It also redacts Telegram's reusable webhook-secret header on
|
||||
successful requests. Secret-sensitive setup errors are replaced before local
|
||||
logging, telemetry, and crash reporting; provider-controlled error names are
|
||||
not trusted. Synthetic serialized HTTP regressions cover mounted API routes,
|
||||
422/500 failures, setup-secret failures, and successful webhook headers.
|
||||
- The failure revealed another usability defect: the toast disappeared and left
|
||||
no explanation in the form. Setup errors are now persistent, redact submitted
|
||||
values, preserve masked inputs, and clear on successful retry. The deterministic
|
||||
browser suite exercises this fail/retry path, not a real Telegram credential.
|
||||
- The safety fix was committed and pushed as `80eaf11ad`, then the isolated
|
||||
instance was restarted on that commit. A deliberately invalid synthetic token
|
||||
was submitted through the actual in-app browser form. Telegram rejected it,
|
||||
the persistent error remained visible, and the input stayed masked. A
|
||||
metadata-only check of the new server log confirmed the canary was absent and
|
||||
the credential envelope was redacted. The synthetic value was then cleared.
|
||||
This verifies the real failure path, not bot authentication or a conversation.
|
||||
|
||||
### Cross-provider quality work
|
||||
|
||||
- Long structured Telegram replies now preserve Markdown as a native `.md`
|
||||
attachment when splitting would damage fences, lists, links, or other block
|
||||
structure. Plain prose still uses readable, lossless chunks. Replacement of a
|
||||
progress message and the attachment send use separate durable publication rows
|
||||
with ordered handoff. This was committed and pushed as `0ebb90145`.
|
||||
- The Teams manifest now includes the required `webApplicationInfo` association
|
||||
for resource-specific consent. This does not add SSO, delegated Graph access,
|
||||
or a requirement to register an Entra Application ID URI. Live Teams still
|
||||
needs an eligible Microsoft 365 tenant and administrative setup.
|
||||
- Slack still needs its existing app credentials connected to the persistent
|
||||
draft and a fresh completed conversation. Historical generic completion text
|
||||
is still treated as a failed quality observation, not release proof.
|
||||
|
||||
### Final automated checkpoint for this resumed pass
|
||||
|
||||
- Full chat integration: **242/242 passed**, no skips, on the fresh migrated
|
||||
database `chat_adapters_test_20260907_synchronized_final`. This includes the
|
||||
manually-created GitHub App fixture and structured Telegram reply transport.
|
||||
- An intermediate run passed 241 tests and failed one lifecycle-recovery
|
||||
assertion. The fixture observed a processed row before its background drain
|
||||
had released the conversation lease. It now waits for that actual lease
|
||||
boundary before injecting the next transaction failure. Exact attempt/state
|
||||
assertions and timeouts are unchanged; no production behavior was altered to
|
||||
make the fixture pass. The final full run above includes that correction.
|
||||
- Focused publication, adapter, setup UI, error handling, and privacy checks:
|
||||
**120 passed, 0 failed**. Four existing real-Sentry-SDK checks were skipped
|
||||
because the SDK could not be loaded in this checkout. Mocked crash-sink input
|
||||
and actual serialized HTTP canary tests ran and passed.
|
||||
- Deterministic provider browser flows: **5/5 passed**, including persistent
|
||||
failure feedback, credential-safe retry, and the Teams consent manifest.
|
||||
These mock provider success; they are not live provider qualification.
|
||||
- Shared, server, and UI TypeScript checks passed. UI production build passed
|
||||
with the existing bundling warnings. Token gates and `git diff --check` passed;
|
||||
`pnpm-lock.yaml` remains untouched.
|
||||
- Independent read-only privacy review confirmed the concrete credential
|
||||
envelope, Telegram header, provider error-name, and HTTP response leaks were
|
||||
covered. Review used synthetic canaries and inspected no real credential
|
||||
stores. A pre-existing arbitrary credential absent from a submitted request
|
||||
cannot be identified by exact-value matching in curated 4xx errors; provider
|
||||
service error redaction remains the upstream boundary for those values.
|
||||
|
||||
No provider is promoted to production-qualified by this checkpoint. The signed
|
||||
GitHub ping and real invalid-token error path are useful live evidence, but all
|
||||
five channels still need fresh completed, provider-visible conversations on the
|
||||
persistent fixture once the remaining credential and tenant gates are resolved.
|
||||
|
||||
### Follow-on Slack credential exposure — 2026-09-07 UTC
|
||||
|
||||
- A fresh signed-in Slack App management session made the existing Signing
|
||||
Secret reveal control respond. The agent copied that value in memory without
|
||||
printing it, but did not submit it to Paperclip.
|
||||
- Navigating to OAuth & Permissions briefly showed a provider load error. The
|
||||
agent then requested a full diagnostic DOM snapshot; before it ran, the page
|
||||
finished loading and exposed the Bot User OAuth Token in tool output. This is
|
||||
an agent qualification-procedure failure, not a Paperclip logger regression.
|
||||
- No Slack credential was submitted to the isolated Paperclip instance. The
|
||||
copied signing-secret variable was cleared. The bot token shown in that
|
||||
snapshot must be revoked and replaced before further use. Do not treat local
|
||||
log cleanup or hiding the provider field as revocation.
|
||||
- The runbook now forbids full snapshots, whole-page text, and screenshots on
|
||||
secret-bearing provider surfaces even during loading/error states. Only
|
||||
explicit nonsecret labels and control metadata may be inspected there; secret
|
||||
entry remains an operator handoff into Paperclip's masked controls.
|
||||
- The operator can revoke the affected `maya-paperclip` OAuth token and repeat
|
||||
the provider installation flow to obtain a replacement. Revocation can remove
|
||||
the bot's channel memberships, so the authorized test channel must be checked
|
||||
and the bot reinvited afterward. See Slack's
|
||||
[token-revocation contract](https://docs.slack.dev/reference/methods/auth.revoke).
|
||||
|
||||
### Parallel hardening and operator handoff — 2026-09-07 UTC
|
||||
|
||||
- Slack now declares the native agent surface, `assistant:write`, and
|
||||
`agent_session_stopped`. Session indicators have a durable, idempotent retry
|
||||
lane independent of message delivery. A delayed status retry recomputes the
|
||||
current published state and cannot revive a cancelled run's working status.
|
||||
Revision, owner, and selected-row fences prevent stale workers from changing
|
||||
a newer result. Working indicators refresh before Slack's one-hour timeout.
|
||||
- Native Slack Stop is authenticated and durably recorded before webhook
|
||||
acknowledgement. It binds the original conversation generation and exact
|
||||
run or queued wake, rechecks the linked user's current authority and reach,
|
||||
and uses provider event time to exclude later work. Cancellation receipts
|
||||
must reflect the authoritative run outcome, including a run that finished
|
||||
before cancellation won the race.
|
||||
- Discord Gateway component acknowledgement now follows durable Paperclip
|
||||
admission. Denied actions are durably audited without a success ACK, and
|
||||
admission retries respect Discord's response deadline. Partial message edits
|
||||
retry their fetch through the same classified provider retry path.
|
||||
- Teams no longer caches user/activity metadata or performs member/Graph
|
||||
lookups before Paperclip admission. Accepted metadata writes are awaited;
|
||||
foreign, missing, conflicting-tenant, and targeted activities fail closed.
|
||||
Setup corrects `groupChat`, exposes implemented mobile commands, and explains
|
||||
that the requested RSC grants deliver every message in an installed team or
|
||||
group chat, while Paperclip's own admission rules constrain retention/work.
|
||||
- Browser access was initially blocked by the locked Mac and later recovered.
|
||||
Safe GitHub App inspection still showed two generated-key records dated
|
||||
`2026-09-07T01:26:23Z` and `2026-09-07T01:28:06Z`. A filename-only Downloads
|
||||
check found no PEM for `paperclip-maya-e2e-0906`; no key contents were read.
|
||||
GitHub stores only the public portion, so a missing private-key download
|
||||
cannot be reconstructed from that page. No extra key was generated or deleted
|
||||
during this inspection.
|
||||
- The operator reported adding Paperclip Maya E2E to Discord. The in-app
|
||||
channel check redirected to an expired Eigenjoy login, so server membership
|
||||
is operator-reported, not independently verified. Paperclip's resumed Discord
|
||||
form has Application ID `1546330979860221952` and Clawd server ID
|
||||
`1457808928258658549` filled in; the bot-token password field remains empty.
|
||||
The operator must enter the token in that masked field, never in this report
|
||||
or the conversation. Server installation alone does not configure Paperclip.
|
||||
|
||||
This remains hardening plus partial setup evidence, not a live round-trip
|
||||
qualification. Fresh provider-visible conversations are still required.
|
||||
|
||||
#### Verified parallel checkpoint
|
||||
|
||||
- Full chat integration: **249/249 passed**, no skips, on the fresh migrated
|
||||
database `chat_adapters_test_20260907_parallel_final`. This includes the
|
||||
exact queued-wakeup-to-run Stop race, late-event and guest denial, status
|
||||
retry/restart/stale-worker fencing, unsupported/permanent-error termination,
|
||||
GitHub and Discord question continuations, Discord FIFO, and Teams denied
|
||||
callback metadata boundaries.
|
||||
- Focused helper, runtime, adapter, publication, OpenAPI, UI contract, and shared
|
||||
catalog tests: **159/159 passed**, no skips.
|
||||
- Deterministic browser flows: **5/5 passed** on the final source tree. An
|
||||
earlier isolated server boot timed out; the subsequent complete run passed
|
||||
in 27.1 seconds. These tests mock provider interactions, not live accounts.
|
||||
- Shared/server/UI TypeScript checks, UI production build, token gates (949
|
||||
files), and `git diff --check` passed. Existing UI bundle-size and mixed-import
|
||||
warnings remain. The broad workspace test suite was not rerun or claimed
|
||||
green; its previously recorded unrelated failures remain outside this proof.
|
||||
- Final fetch confirmed `origin/master` at `856813ba3` is already an ancestor
|
||||
of the working branch. No rebase was necessary, no other worktree was used,
|
||||
no PR was changed, and `pnpm-lock.yaml` remains untouched.
|
||||
- Unsupported Slack session status now settles until new conversation activity
|
||||
restages it, rather than polling completed threads forever. Definite
|
||||
permission/destination failures are separately visible in Activity and do
|
||||
not resend message content.
|
||||
- Teams reaction/action/modal metadata recording was moved behind the actual
|
||||
authorization boundary. The regression checks both rejected callbacks with
|
||||
a valid route and accepted callbacks with the same route. Admitted lifecycle
|
||||
changes retain regional reply-route refresh without retaining user metadata.
|
||||
|
||||
The operator-reported Discord install still requires a bot token entered into
|
||||
Paperclip and a restored Eigenjoy browser session for live provider proof.
|
||||
GitHub still needs its private PEM; Slack and Telegram need the previously
|
||||
documented exposed tokens rotated; Teams needs an eligible tenant/admin setup.
|
||||
None of these gates is represented as a successful live conversation.
|
||||
|
||||
#### Test ingress renewed after the verified-code restart
|
||||
|
||||
- The isolated server was restarted with verified code `f2724d8f2`; its private
|
||||
health response reports that commit and ready startup recovery.
|
||||
- The old quick tunnel expired (`Unauthorized: Tunnel not found`) while its
|
||||
process kept reconnecting. It was replaced with
|
||||
`https://doctor-files-whole-concepts.trycloudflare.com`. This supersedes the
|
||||
earlier `tile-daily-angle-rather` hostname for the live fixture.
|
||||
- The existing webhook-only proxy still rejects the public board health and
|
||||
company API paths with **404**. A recognized unsigned GitHub `ping` reaches
|
||||
Paperclip and returns **401**. No local-trusted board/API was exposed.
|
||||
- GitHub App `paperclip-maya-e2e-0906` now has its existing webhook URL updated
|
||||
to the replacement host, with the same endpoint public ID and secret. The
|
||||
provider displayed its successful saved-app notice; no credential was read,
|
||||
generated, rotated, or deleted during that URL update.
|
||||
- The GitHub Paperclip form has App ID `4853886` filled in and still needs the
|
||||
operator's PEM. The Discord form retains its known application/server IDs and
|
||||
still needs the bot token. This does not establish a successful agent run.
|
||||
|
||||
### Webhook/board separation and credential-entry polish — 2026-09-07 UTC
|
||||
|
||||
- A live-readiness audit found that a webhook-only tunnel was also being used
|
||||
as the board origin. That produced valid-looking Paperclip links whose host
|
||||
intentionally returned 404. `PAPERCLIP_CHAT_WEBHOOK_PUBLIC_URL` now controls
|
||||
only provider callback URLs; the board origin still controls authentication,
|
||||
identity confirmation, task links, and trusted hosts. Invalid explicit ingress
|
||||
URLs refuse startup without echoing their value. Local/private task links are
|
||||
omitted with neutral instructions, not redirected to ingress or displayed as
|
||||
`[link removed]`. Config-file-only board URLs work for question cards too.
|
||||
- GitHub setup now imports a downloaded PEM directly into the in-memory
|
||||
credential field, with a 64-KiB limit, persistent safe errors, and revision
|
||||
fencing against slower file reads, later paste, and unmount/provider changes.
|
||||
Connect is disabled during import. A real deterministic browser check caught
|
||||
the previous CSS-masked textarea exposing its contents as page text. The
|
||||
default is now a password input; an actual multiline textarea exists only
|
||||
during explicit reveal. Both pasted and imported PEMs reach configure
|
||||
byte-for-byte. Only synthetic keys were involved in this test.
|
||||
- Discord component denials now send one fixed private remediation after the
|
||||
denial is durable and before the acknowledgement deadline. Duplicate accepted
|
||||
callbacks still acknowledge normally; late denials do not respond; reply
|
||||
failure is not retried or logged with provider content.
|
||||
- The first combined integration run was 250/251. Its Telegram helper raced a
|
||||
concurrently scheduled terminal-card drain: the requested next question was
|
||||
subsequently published once, nine milliseconds after creation, with one
|
||||
attempt and no delivery error. The helper now waits for its own durable
|
||||
publication state; no production retry/ordering rule or timeout was weakened.
|
||||
|
||||
Final combined verification for these changes:
|
||||
|
||||
- **251/251** full chat integration tests, zero skips, on fresh database
|
||||
`chat_adapters_test_20260907_origin_verified`.
|
||||
- **83/83** focused server/config/provider/link tests and **14/14** focused UI
|
||||
tests; **5/5** deterministic browser cases, including the actual file chooser,
|
||||
imported/pasted credential payloads, reveal/hide, and error recovery.
|
||||
- Shared, server, and UI typechecks passed. Design token gates and diff checks
|
||||
passed. The broad workspace suite was not rerun and is not claimed green.
|
||||
- Reports are retained under `.paperclip-runtime/chat-adapters-live/` as
|
||||
`origin-verified-integration.json`, `origin-final-unit.json`,
|
||||
`origin-verified-ui-unit.json`, and `origin-verified-browser.log`.
|
||||
|
||||
These checks do not replace live provider qualification. Discord still needs a
|
||||
bot token entered into Paperclip and a renewed provider login; GitHub needs its
|
||||
PEM and repository installation. Slack's signed-in OAuth page is reachable but
|
||||
its exposed test token still requires replacement and write-only entry. Telegram
|
||||
and Teams retain their previously documented rotation and tenant gates.
|
||||
|
||||
Runtime checkpoint after commit `f535dde54`:
|
||||
|
||||
- The combined fixes were committed and pushed to `codex/chat-adapters`; the UI
|
||||
production build also passed (existing chunk-size warnings only).
|
||||
- The isolated 3103 server reports `f535dde54` and ready startup recovery. Its
|
||||
board/auth origin is `http://127.0.0.1:3103`; only
|
||||
`PAPERCLIP_CHAT_WEBHOOK_PUBLIC_URL` uses the current Cloudflare ingress.
|
||||
- GitHub setup still advertises the exact existing public webhook path. Public
|
||||
health and company API checks remain **404**; an unsigned recognized GitHub
|
||||
`ping` remains **401**. No board trust or exposure was broadened.
|
||||
- The in-app GitHub form was checked without reading credentials: its default
|
||||
key control is `type=password`, no plaintext textarea is mounted, and
|
||||
**Choose .pem file** is present. Both provider forms still have empty secret
|
||||
fields; the known GitHub App ID and Discord application/server IDs were
|
||||
filled again after the development reload. The setup tabs remain available
|
||||
for the operator's write-only credential handoff.
|
||||
|
||||
### GitHub private fixtures and installation completed — 2026-09-07 UTC
|
||||
|
||||
The signed-in in-app browser completed the remaining pre-credential setup:
|
||||
|
||||
- Created private, disposable repositories
|
||||
[`cryppadotta/paperclip-chat-e2e-enabled`](https://github.com/cryppadotta/paperclip-chat-e2e-enabled)
|
||||
(ID `1359763399`) and
|
||||
[`cryppadotta/paperclip-chat-e2e-disabled`](https://github.com/cryppadotta/paperclip-chat-e2e-disabled)
|
||||
(ID `1359763710`). Both contain only their initial README; no production data,
|
||||
existing repository contents, or generated agent work was added. They are kept
|
||||
for the pending positive/negative reach tests, not deleted during setup.
|
||||
- Installed the existing **Paperclip Maya E2E 0906** App on that account as
|
||||
[installation `159668881`](https://github.com/settings/installations/159668881).
|
||||
The resulting installation settings visibly retained **Only select
|
||||
repositories**, with remove controls for exactly the two new fixtures.
|
||||
Permissions are Metadata read, Issues read/write, and Pull requests read/write.
|
||||
No existing repositories or all-repositories access were granted.
|
||||
- The current ingress received a GitHub webhook and returned **200** at
|
||||
`2026-09-07T04:47:40Z`. Paperclip remains draft and disabled with zero endpoint
|
||||
resources/conversations, null bot/installation identity, and the earlier signed
|
||||
ping timestamp unchanged. This is the intended pre-PEM boundary: draft
|
||||
endpoints accept only setup ping processing; installation events are ignored
|
||||
without a retained body or new ingress action. The installation will be
|
||||
discovered authoritatively through GitHub's API during credential configure.
|
||||
The 200 alone is not proof of authenticated installation ingestion or a chat.
|
||||
- GitHub's private PEM remains absent from the masked setup field. No additional
|
||||
private key was created or read. Discord's developer page was rechecked and
|
||||
shows **Choose an account** / **Please log in again**; its Paperclip token field
|
||||
is still empty. The parallel audit found no pre-credential live path remaining
|
||||
for Slack, Telegram, or Teams beyond their documented human-controlled gates.
|
||||
|
||||
This advances GitHub setup only. A real issue/PR message, agent run, reply,
|
||||
reaction, question continuation, and the recovery/governance matrix remain
|
||||
unqualified until the App PEM is entered and Paperclip connects.
|
||||
|
||||
The corresponding pre-PEM installation regression and the complete chat
|
||||
integration suite passed **252/252**, zero skips, on fresh database
|
||||
`chat_adapters_test_20260907_github_install_draft`; report:
|
||||
`.paperclip-runtime/chat-adapters-live/github-install-draft-integration.json`.
|
||||
Only the regression and evidence documentation changed in this checkpoint;
|
||||
the running, previously browser-qualified implementation remains `f535dde54`.
|
||||
|
||||
### Discord connection repair and GitHub credential qualification — 2026-09-07
|
||||
|
||||
The user-reported Discord **Invalid Form Body** failure was a real request-shape
|
||||
defect: guild-member lookup used `@me` where Discord requires a numeric user ID.
|
||||
The corrected request uses the already-verified bot ID. Live setup then succeeded
|
||||
with the existing token and reached **Try Maya E2E in Discord**; no token reset
|
||||
was needed. The separate Discord chat session still requires Eigenjoy login, so
|
||||
native message/thread/run qualification has not advanced beyond connection.
|
||||
|
||||
GitHub accepted the user-authorized PEM import through Paperclip's file chooser.
|
||||
Its live issue mention created CHA-1, received a receipt reaction, and received
|
||||
the expected guest-isolation refusal rather than an agent answer. The private
|
||||
identity confirmation for `cryppadotta` to the local Board account is staged for
|
||||
the user; that permission grant has not been confirmed. Recovery copy now
|
||||
correctly explains that an administrator creates the private identity link.
|
||||
|
||||
Tailscale is connected, but Funnel requires tailnet enablement. The pending
|
||||
request targets only the webhook-only proxy on port 3104 through HTTPS port
|
||||
10000; existing tailnet-only routes are unchanged. Until that administrative
|
||||
step completes, GitHub remains on the current Cloudflare webhook ingress and
|
||||
the board remains local/private. No stable Tailscale webhook success is claimed.
|
||||
|
||||
Verification after the fixes:
|
||||
|
||||
- Focused Discord and run-publication unit tests: **19/19**.
|
||||
- Server `tsc --noEmit`: passed.
|
||||
- Fresh full chat integration: **252/252**, zero skips, database
|
||||
`chat_adapters_test_20260907_discord_member_02`; report
|
||||
`.paperclip-runtime/chat-adapters-live/discord-member-integration-20260907-02.json`.
|
||||
- The first fresh run was **251/252** because a Slack exact-redelivery test
|
||||
sampled its transport count before prior durable denial effects finished.
|
||||
The test now waits for those effects and additionally proves redelivery
|
||||
creates no new effect row; no production queue behavior was relaxed.
|
||||
- Live browser checks covered real provider credential verification and the
|
||||
GitHub guest-refusal round trip, not a successful agent conversation. The
|
||||
broader deterministic browser suite was not rerun for these server changes.
|
||||
|
||||
### Stable ingress and linked-account qualification — 2026-09-07, continued
|
||||
|
||||
The operator completed Tailscale Funnel enablement and the GitHub identity
|
||||
confirmation. These observations supersede the pending gates above:
|
||||
|
||||
- The stable webhook origin is
|
||||
`https://dottas-macbook-pro.tail29c1aa.ts.net:10000`. Funnel forwards only to
|
||||
the webhook-only proxy on loopback port 3104. Existing tailnet-only routes on
|
||||
443 and 8443 were not made public. Public board health/company requests
|
||||
return **404**, and an unsigned recognized GitHub ping returns **401**.
|
||||
- GitHub's App settings and Paperclip now use that stable origin with the
|
||||
existing endpoint path and signing secret. A signed, real issue comment
|
||||
reached Paperclip through Tailscale. The obsolete temporary Cloudflare
|
||||
tunnel was stopped after this positive ingress evidence.
|
||||
- The private confirmation flow linked `cryppadotta` to the local Board account.
|
||||
A new conversation, rather than the earlier guest-admitted CHA-1, was used
|
||||
for the linked-account test.
|
||||
- [Enabled-repository issue 2](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/issues/2#issuecomment-5571135634)
|
||||
created exactly one conversation and task **CHA-2** and received a receipt
|
||||
reaction. **This was not a successful agent-answer test:** the pinned Codex
|
||||
ACP runtime converted an unsupported-model provider error into assistant
|
||||
text and reported the run as completed. Paperclip then published that raw
|
||||
diagnostic. This is a release-blocking error-classification/publication
|
||||
defect, not acceptable chat output.
|
||||
- The installed `codex-acp` 1.6.2 process runs its bundled Codex 0.148.0, not the
|
||||
separately installed CLI. The test agent had inherited the operator's Astra
|
||||
model. Only the isolated Maya fixture was pinned to Paperclip's existing
|
||||
`gpt-5.6-sol` default for further qualification; no global model, CLI,
|
||||
credential, or unrelated agent configuration was changed. Successful live
|
||||
runtime execution still needs proof after the typed-failure repair.
|
||||
- [Disabled-repository issue 1](https://github.com/cryppadotta/paperclip-chat-e2e-disabled/issues/1#issuecomment-5571234021)
|
||||
received an explicit bot mention. GitHub delivery
|
||||
`9b1d68a0-aabe-11f1-80a1-0922ed513425` returned **200**, body **ignored**.
|
||||
The repository remains disabled in Paperclip, with no conversation or task
|
||||
created. This is provider-backed negative-reach evidence, not merely an
|
||||
absence of a visible reply.
|
||||
- GitHub's real redelivery control resent the existing CHA-2 root delivery
|
||||
`8c9c7240-aabd-11f1-86a6-ed31986fb576`. Tailscale ingress returned **202** at
|
||||
`2026-09-07T13:27:39Z`. Before/after counts were unchanged: two endpoint
|
||||
conversations, three CHA-2 runs, two CHA-2 publications, and six CHA-2
|
||||
comments. Redelivery did not create another task, wakeup, or publication.
|
||||
- Discord's signed-in browser session now reaches Clawd. A real root mention
|
||||
created its native thread and **CHA-3**, with a receipt reaction and the
|
||||
expected safe guest-isolation refusal. Eigenjoy was subsequently linked to
|
||||
the local Board account through the private confirmation flow. A fresh
|
||||
linked Discord thread is still required; CHA-3 retains its original guest
|
||||
trust classification.
|
||||
|
||||
No provider secret, private key, clipboard value, or one-time confirmation URL
|
||||
is recorded here. Neither GitHub nor Discord is being declared fully qualified
|
||||
from connection, receipt, or guest-refusal evidence alone.
|
||||
|
||||
### Real final replies and queue-quality findings — 2026-09-07, 13:45 UTC
|
||||
|
||||
The shared typed ACP terminal-error repair was committed and pushed as
|
||||
`1325329e3`. Both supported acpx patches now negotiate typed session-failure
|
||||
metadata and fail closed on terminal errors rather than treating their raw
|
||||
provider diagnostics as an assistant answer. Warnings and ordinary quoted
|
||||
error-like content are not classified by text matching. The broad focused ACP
|
||||
regression slice passed **211/211**, with zero skipped cases.
|
||||
|
||||
The next live run exposed a second, independent defect: the model returned the
|
||||
requested exact answer, but Paperclip selected an earlier bookkeeping comment
|
||||
for publication. The working-tree fix gives the runner-selected final sole
|
||||
ownership of the external response for chat-origin runs. Intermediate comments
|
||||
remain internal, and a yielded or missing final cannot publish an internal note
|
||||
as a fallback. Explicit Board **Send to channel** remains a separate action.
|
||||
|
||||
The isolated server restarted at `2026-09-07T13:44:55Z` with that fix, durable
|
||||
Discord receipt removal, and independent reconciliation lanes. Real UI tests
|
||||
then verified:
|
||||
|
||||
- GitHub's unmentioned follow-up stayed on CHA-2. Run
|
||||
`b7190e01-0176-4af7-a471-c1e013c2a015` succeeded and
|
||||
[reply 5571558895](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/issues/2#issuecomment-5571558895)
|
||||
contained exactly `GH-LIVE-0907-ROUNDTRIP-OK`.
|
||||
- Discord's linked root created CHA-4 and native thread `1546513811672932372`.
|
||||
An unmentioned follow-up stayed in that task; run
|
||||
`2443fa37-ea1e-436b-a1af-3ad6e58afc51` succeeded and
|
||||
[reply 1546516692031504485](https://discord.com/channels/1457808928258658549/1546513811672932372/1546516692031504485)
|
||||
contained exactly `DISCORD-LIVE-0907-ROUNDTRIP-OK`. Its receipt reaction
|
||||
cleared after the terminal reply.
|
||||
- Both setup wizards completed through their real **I've sent the test
|
||||
message** controls; both endpoints are now `active` with setup complete.
|
||||
|
||||
These are successful core live replies, not a full production-quality pass.
|
||||
The follow-on observation found that generic stranded-task recovery incorrectly
|
||||
started an extra run after each completed turn. The tasks intentionally stay
|
||||
`in_progress` while their external conversations wait for another user message;
|
||||
that state was mistaken for unfinished productive work. A narrow recovery
|
||||
repair and live no-extra-run retest are still pending at this checkpoint.
|
||||
|
||||
The focused server checks passed **70/70** and deterministic browser checks
|
||||
passed **5/5** on the final-selection/scheduler/receipt changes. Shared, server,
|
||||
UI, adapter-utils, and codex-local TypeScript checks passed. The fresh full
|
||||
chat integration run is being repeated after its synthetic final-response
|
||||
fixtures were updated to the new explicit runner-selection contract. These
|
||||
figures do not claim the repository-wide suite or remaining live matrix passed.
|
||||
|
||||
### Ordered bursts and recovery regression — 2026-09-07, 13:58 UTC
|
||||
|
||||
After restarting the isolated server at `13:54:51Z` with the chat durable-wait
|
||||
guard, three messages were sent rapidly through each real provider UI. All six
|
||||
inbound comments persisted in provider order on the existing CHA-2 and CHA-4
|
||||
tasks. Each provider started one run for the first message and coalesced the
|
||||
two following messages into one durable deferred wake and one subsequent run.
|
||||
GitHub returned `DELTA`, then exactly `DELTA EPSILON`; Discord returned its
|
||||
first-word acknowledgement, then exactly `ALPHA BETA`. The separate threads
|
||||
did not mix their code words. Discord cleared all three working receipts.
|
||||
|
||||
The four causal runs succeeded. No unsolicited recovery run appeared in the
|
||||
post-burst observation. Both tasks were marked done by the agent, however, so
|
||||
that absence alone does not prove the narrower in-progress chat-wait guard.
|
||||
A live keep-open retest remains necessary. The first runs took about 78–83
|
||||
seconds, and the queued runs took about 15 seconds for GitHub and 50 seconds
|
||||
for Discord. Ordering and correctness passed; those observed delays are not
|
||||
an instantaneous-chat performance claim.
|
||||
|
||||
The final fresh chat integration suite passed **255/255**, with zero skips,
|
||||
on `chat_adapters_test_20260907_live_hardening_05`. The final full process
|
||||
recovery suite passed **133/133**, with zero skips, including active/waiting
|
||||
chat idle behavior, completed-conversation recovery, ordinary non-chat
|
||||
recovery, and pending in-review participant recovery. The production guard
|
||||
requires an in-progress task, a successful external-chat run, and its
|
||||
company/issue-bound active or waiting conversation; explicit queued work
|
||||
is checked first and remains runnable.
|
||||
|
||||
### Clean keep-open proof and split webhook topology — 2026-09-07, 13:59 UTC
|
||||
|
||||
The clean-source keep-open retest on revision `5bd9c0d55` supersedes the
|
||||
remaining recovery caveat above:
|
||||
|
||||
- GitHub run `c3335bdf-6a2e-49a5-82eb-8d31df92e4d0` ran from
|
||||
`13:59:33.398Z` to `13:59:39.464Z` on the existing CHA-2 conversation.
|
||||
[Bot comment 5571729974](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/issues/2#issuecomment-5571729974)
|
||||
contained exactly `GITHUB-IDLE-WAIT-OK`.
|
||||
- Discord run `f8c9dbe2-7e94-469d-8345-717eb7dad1bf` ran from
|
||||
`13:59:31.087Z` to `13:59:38.234Z` in native thread
|
||||
`1546513811672932372`.
|
||||
[Reply 1546520298793468036](https://discord.com/channels/1457808928258658549/1546513811672932372/1546520298793468036)
|
||||
contained exactly `DISCORD-IDLE-WAIT-OK`.
|
||||
|
||||
Both tasks intentionally remained `in_progress` with active conversations for
|
||||
more than eight minutes after those terminal replies. Neither received an
|
||||
additional run. This is the missing live proof that the recovery guard leaves
|
||||
healthy external-chat tasks idle until new inbound or explicitly queued work
|
||||
arrives.
|
||||
|
||||
The public callback topology is now split without exposing the Board:
|
||||
|
||||
- HTTPS `:8443` is the canonical Telegram webhook origin and forwards only to
|
||||
the loopback webhook proxy on port 3104.
|
||||
- HTTPS `:10000` remains available for the existing Slack and GitHub callback
|
||||
URLs and forwards through the same webhook-only proxy.
|
||||
- HTTPS `:443` remains tailnet-only for the private Board. Public health,
|
||||
company API, and other Board routes are not forwarded by either webhook
|
||||
listener.
|
||||
|
||||
The latest setup-edge full chat integration suite passed **258/258**, zero
|
||||
skips. The combined process-recovery/status-payload suite passed **135/135**,
|
||||
zero skips. The deterministic browser suite passed **5/5** on clean revision
|
||||
`5bd9c0d55`, before the latest setup-edge/UI changes; it is still pending on
|
||||
the current working tree, so this checkpoint does not claim a current browser
|
||||
pass.
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
# Images and files — live qualification, September 7, 2026
|
||||
|
||||
This is an incremental evidence log, not a blanket production-readiness claim.
|
||||
Live provider actions use the signed-in in-app browser. The isolated Paperclip
|
||||
instance is on loopback port 3103; only verified webhooks are publicly routed.
|
||||
|
||||
## Reproduced user failure
|
||||
|
||||
Discord CHA-4 run `9b90ddaa-6d82-4685-84b1-9483c30de346` generated and uploaded a
|
||||
2,111,878-byte PNG. Artifact `43104a30-4ae5-4078-a880-68c9f9720318` pointed to
|
||||
attachment `7abdf671-1eb2-402a-8417-274b048c39ed`, but the attachment had no comment
|
||||
binding. The run's final publication contained no attachment IDs. The bot's claim
|
||||
that the image was shown was false. Both the npm CLI attempt and a workspace-local
|
||||
CLI fallback failed. The image itself was intact in Paperclip storage.
|
||||
|
||||
The audit also found a second path: a successfully bound, during-run attachment
|
||||
could remain internal when a different final presentation comment was published.
|
||||
An explicit same-run attachment handoff and a bundled API-based artifact helper
|
||||
now pass the normal live workflow below. Independent review additionally hardened
|
||||
immutable upload provenance, the per-turn file cap, and helper retries.
|
||||
|
||||
## Native transport checks
|
||||
|
||||
One known, non-sensitive orange-cat PNG and a 128-byte text fixture were uploaded
|
||||
through Paperclip's Board attachment API and explicitly sent to each existing QA
|
||||
conversation. This isolates native transport from agent-generation/handoff logic;
|
||||
it does **not** prove the agent handoff fix.
|
||||
|
||||
| Provider | Observed outcome |
|
||||
| --- | --- |
|
||||
| Discord | Cat rendered in the native media viewer; text file rendered with its exact contents. Image message `1546531868575535114`; file message `1546531871523995698`, in thread `1546513811672932372`. |
|
||||
| Slack | Bot image loaded at 1024×1024 and text file preview contained the exact fixture contents in the existing CHA-6 thread. |
|
||||
| Telegram | Bot image loaded at 800×800; document message `417200359:11` downloaded through the actual UI. The downloaded 128-byte file matched the source SHA-256 exactly. |
|
||||
| GitHub | App comment transport is link-only for attachments; direct upload is not qualified. Live Board file send published a caption and one explicit private-task notice per selected file, starting with comment `5572594232`. No file bytes or loopback URLs were exposed. The misleading generic `Shared filename` preface was replaced and the final live retake verified the neutral wording below. |
|
||||
| Teams | No live media claim: Microsoft 365 tenant/admin setup remains unavailable. |
|
||||
|
||||
Text fixture SHA-256:
|
||||
`fd40030afb62b83181a2a46dde8220e8defecfa0b4328e380c30b1899ccdce24`.
|
||||
Telegram's browser download event timed out, but the host download appeared in
|
||||
Downloads at 09:46:27 local time and its size/hash verified successfully. This was
|
||||
a browser event-observation limitation, not a failed file delivery.
|
||||
|
||||
## Inbound inspection checks
|
||||
|
||||
Files were uploaded through each provider's real message composer. The bot was
|
||||
asked to inspect actual bytes, not infer content from filenames.
|
||||
|
||||
- Slack: run `10aa0f41-0cc3-4997-95b4-f50eb1e033e8` succeeded, identifying the orange
|
||||
tabby/green eyes and reading `cobalt otter 47.`. Both stored attachments were
|
||||
bound to inbound comment `cfb14fe0-f463-4952-9cf9-2acdc32997b2`. Final bot message
|
||||
`1788792053.513999` is in root thread `1788789960.341109` in `C0BUT55N9RV`.
|
||||
The run took about 135 seconds; this remains a usability concern.
|
||||
- Telegram photo: run `03f06e17-a0e5-43e5-a894-0cea68566aa3` identified the cat,
|
||||
eyes/nose, sofa, plant and window from the inbound JPEG. Final message
|
||||
`417200359:6`; about 132 seconds.
|
||||
- Telegram document: a follow-up sent while the image run was active queued and
|
||||
then ran as `41215211-debf-44cc-9b93-a220fd0931de`. It returned the exact phrase
|
||||
in `417200359:8`; about 81 seconds after execution began. The two messages stayed
|
||||
on CHA-8 and produced separate, correctly ordered responses.
|
||||
- GitHub private issue upload: native UI produced an HTML image plus a Markdown
|
||||
text-file link. Human comment `5572301393`, bot `5572302077`, run
|
||||
`0c252a02-51cc-4aeb-b829-73865415070e`. The bot did not claim to inspect unavailable
|
||||
bytes, but described the active chat connection as unavailable and requested
|
||||
a separate tool connection. This is **not** a successful inbound media check;
|
||||
chat transport must explain its file/link limitations clearly.
|
||||
- Discord: run `888e586d-b62e-454c-bb77-d4d0c14ea245` inspected both inbound files,
|
||||
identified the cat/green eyes/sofa/plant, and read the exact phrase. Final bot
|
||||
message `1546532360630177873`, about 146 seconds after execution began.
|
||||
|
||||
## Normal agent handoff retake
|
||||
|
||||
After restarting the local server with the handoff fix at 14:56:49 UTC, each
|
||||
existing provider conversation received an ordinary request to return the cat
|
||||
and create a text file with a provider-specific exact marker. The requests did
|
||||
not tell Maya which tool or helper command to use. All three stayed on their
|
||||
existing task, succeeded, and published both selected attachments.
|
||||
|
||||
| Provider | Run and real-provider proof |
|
||||
| --- | --- |
|
||||
| Discord | Run `448779d2-73a3-4f39-9f75-0c9fdac528d0`, 14:57:10–15:02:43 UTC. Native image message `1546536207448547401` loaded; native file `1546536210195808318` previewed exactly `DISCORD-FILE-HANDOFF-0907-OK`. |
|
||||
| Slack | Run `d4b00e25-a2b6-49bd-9442-384419c88776`, 14:57:17–15:02:00 UTC. Both native files appeared in CHA-6's original thread; the image loaded at 1024×1024 and the file preview showed `SLACK-FILE-HANDOFF-0907-OK`. |
|
||||
| Telegram | Run `949a2b1a-5f6c-4680-971c-cceb244be8a5`, 14:57:23–15:02:24 UTC. Image `417200359:14` loaded at 800×800. Document `417200359:15` downloaded through Telegram's real UI; its 29 bytes were exactly `TELEGRAM-FILE-HANDOFF-0907-OK`, without a trailing newline. |
|
||||
|
||||
The Telegram download SHA-256 was
|
||||
`a0692bcddade1e6e9e1a15ee975c2c2d501be8bdc34c5e1cbe84b3de4e7b2f7f`.
|
||||
Paperclip's outbox independently showed all six attachment publications as
|
||||
`published`, one image and one file per provider, with no duplicate file sends.
|
||||
The final prose said the files were **prepared**, not falsely provider-confirmed.
|
||||
|
||||
This repairs the reported missing-image failure, but the 283–333 second agent
|
||||
turns are too slow for a polished simple file reply. The Discord run made 28
|
||||
completed/failed tool calls, including avoidable connection discovery. The task
|
||||
prompt now explicitly directs external file replies to the installed artifact
|
||||
helper and away from provider-tool discovery or fetching a CLI. The final retake
|
||||
below measures the improvement; native delivery success does not prove the
|
||||
interaction is fast enough.
|
||||
|
||||
The Paperclip task transcript also passed a live UI check: inbound images and
|
||||
files appeared even when the comment had no Markdown reference, the image opened
|
||||
in the gallery at full size, and the text-file link opened its exact content.
|
||||
|
||||
## Implemented hardening
|
||||
|
||||
- Render provider-bound comment images/files in the task transcript, even when
|
||||
its caption contains no Markdown attachment reference.
|
||||
- Include bounded, task/comment-scoped attachment descriptors in wake context so
|
||||
agents can discover and download the files without searching the whole task.
|
||||
- Carry only explicitly selected same-agent/same-run attachments into final chat
|
||||
delivery. Never infer authorization from an unbound artifact alone.
|
||||
- Explain GitHub's link-only behavior before an explicit Board file send and in
|
||||
the provider fallback. Do not expose loopback URLs or publish private files to
|
||||
an unrelated public upload service.
|
||||
- Record immutable originating-run attribution on upload; never derive authority
|
||||
from editable work-product records or backfill ambiguous legacy files.
|
||||
- Serialize both comment binding and direct-to-comment uploads. Reject a
|
||||
twenty-first chat file with an actionable error rather than silently dropping
|
||||
one; preserve ordinary non-chat multi-comment uploads.
|
||||
- Recover matching uploads using immutable origin and exact content hash. Local
|
||||
concurrent helpers serialize; ambiguous network/408/5xx/malformed-success
|
||||
outcomes fail closed until the durable attachment is found or an operator
|
||||
explicitly accepts duplicate risk. This is not cross-host exactly-once upload.
|
||||
- Post a single generation-fenced notice after a definite supported-provider
|
||||
file rejection, without replaying an ambiguously delivered file.
|
||||
|
||||
## Automated checkpoint before upstream reconciliation
|
||||
|
||||
- Fresh database chat integration: **262/262**, no skips.
|
||||
- Focused server provider/projection/attachment tests: **313/313**.
|
||||
- Executable artifact helper retry/concurrency tests: **18/18**.
|
||||
- Focused UI tests: **120/120**; deterministic provider browser flows: **5/5**.
|
||||
- Recovery/status/context checkpoint: **153/153**, using an explicit fresh
|
||||
PostgreSQL database instead of silently skipping unsupported embedded tests.
|
||||
- Attachment wake-context scope/quarantine database checks: **6/6**, no skips.
|
||||
- Migration snapshot drift: **1/1**. Workspace typecheck, workspace build, and
|
||||
UI token gates passed. These build checks precede the final provenance edits;
|
||||
final targeted compile is repeated before handoff.
|
||||
|
||||
## Final merged-build retake
|
||||
|
||||
Merged `origin/master` at `f6a211479`, retained the media hardening, and corrected
|
||||
the connection wizard's tool-method selector after reconciliation. Restarted the
|
||||
live server with migration 0249 applied. Three ordinary requests were sent from
|
||||
the signed-in provider composers at 15:30:25–27 UTC, without helper instructions.
|
||||
|
||||
| Provider | Observed result on the final media implementation |
|
||||
| --- | --- |
|
||||
| Discord | Run `15f7af18-d052-44d3-9698-127433b9e941` succeeded in 163 seconds. Native image `1546543862883946597` visibly rendered the cat; file `1546543864142102529` previewed `DISCORD-MEDIA-FINAL-0907-OK`. |
|
||||
| Slack | Run `d5e7b996-1fc0-41e4-92fd-c5522fd23fbb` succeeded in 183 seconds. Native file message `1788795212.198169` previewed `SLACK-MEDIA-FINAL-0907-OK`; image message `1788795215.443269` visibly rendered the cat in the same thread. |
|
||||
| Telegram | Run `783a9af6-eefd-4d24-a39b-ce8eac97bdcf` succeeded in 151 seconds. Photo `417200359:18` loaded at 800 pixels wide; document `417200359:19` downloaded through the real UI. |
|
||||
| GitHub | Fresh Board file send `4a82fa40-5fc0-42f7-99ac-ddc97c5b2ff8` produced comment `5572840135`: the file is saved on the private Paperclip task and this GitHub App connection cannot upload file bytes into comments. No misleading “Shared” preface or public file URL. |
|
||||
|
||||
All six native attachment publications were `published` with one attempt each;
|
||||
each upload carried the correct immutable originating run. The refreshed
|
||||
Paperclip task transcript showed the newly bound images/files, and the native
|
||||
provider threads showed one copy of each selected file. GitHub's first fallback
|
||||
retake attempted to reuse already comment-bound attachment IDs and correctly
|
||||
received 409; a fresh QA upload was used instead, not a forced rebinding.
|
||||
|
||||
The downloaded Telegram file was 29 bytes with SHA-256
|
||||
`464d31c3110370919f443cfb3576b836812f8590dd3bbf8572352d2cf4ed3136`, exactly matching
|
||||
Paperclip's stored asset. It contained the requested marker **plus a trailing
|
||||
newline**. The transport preserved the bytes correctly, but this is not an
|
||||
exact-byte content-generation success. Discord's text also included a newline;
|
||||
Slack's 25-byte marker had none. Do not silently rewrite generated file bytes in
|
||||
the transport to hide a model-content mismatch.
|
||||
|
||||
Functional delivery is repaired. The 151–183 second turns improved substantially
|
||||
from 283–333 seconds, but remain too slow for a polished simple file reply.
|
||||
GitHub private inbound attachment bytes remain unqualified and the current
|
||||
outbound adapter remains link-only. Teams remains live-unqualified without the
|
||||
Microsoft 365 tenant/admin setup; no universal “files work everywhere” claim.
|
||||
|
||||
Post-merge verification:
|
||||
|
||||
- Connection/GitHub/tool-access/migration regression slice: **387/387**, no skips.
|
||||
- Workspace typecheck and workspace build: passed on the final merged sources.
|
||||
- Deterministic provider browser flows: **5/5** on another fresh database.
|
||||
- UI token gates and `git diff --check`: passed.
|
||||
- Native-session and adapter-registry tests: **158/158**.
|
||||
|
||||
Runtime reproducibility caveat: these live Maya retakes used the retained ACP
|
||||
installation resolving Codex 0.148.0 with the Sol fixture model. The merged
|
||||
manifest now requests 0.153.4, also installed as the global CLI. The local
|
||||
dependency tree was not re-resolved during handoff, because doing so without the
|
||||
CI-owned lockfile would also refresh ranged transitive dependencies and change
|
||||
the just-qualified environment. Neither the lockfile nor Maya's model/engine
|
||||
was modified. The next normal dependency refresh must requalify the current
|
||||
runtime; these results do not establish that 0.153.4 ACP combination.
|
||||
|
||||
The broad workspace run was stopped after fixture/mock failures and does not
|
||||
have a passing final summary. It also overlapped upstream reconciliation, so it
|
||||
is not a valid final-tree checkpoint. Only the explicit focused runs above are
|
||||
claimed green.
|
||||
|
||||
GitHub's [issue-comment REST API](https://docs.github.com/en/rest/issues/comments#create-an-issue-comment)
|
||||
accepts a comment body, unlike the browser's separate
|
||||
[file attachment workflow](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/attaching-files).
|
||||
The shipped adapter's link-only behavior is a scoped product limitation; it is
|
||||
not evidence that every possible GitHub integration can never transfer files.
|
||||
|
|
@ -0,0 +1,642 @@
|
|||
# Native chat Board files and webhook recovery — 2026-09-07
|
||||
|
||||
## Environment and scope
|
||||
|
||||
Isolated Board `http://127.0.0.1:3103`, company Chat Adapter E2E, snapshot 10,
|
||||
loaded server `2026.831.0+396.git.dde176bbc`. The branch HEAD was `66a68fee5`
|
||||
(documentation-only after the running implementation). Maya E2E remained
|
||||
`paperclip_runner` → `codex` → `gpt-5.6-luna`.
|
||||
|
||||
These are real signed-in in-app browser checks against the configured Slack,
|
||||
GitHub, Discord, and Telegram sandboxes. They deliberately start no model turns:
|
||||
the Codex account limit still prevents additional native model qualification.
|
||||
They do not qualify Teams, which still needs an eligible tenant/admin setup.
|
||||
|
||||
## Bounded webhook outage
|
||||
|
||||
Paused only the owned webhook proxy process with `SIGSTOP` at
|
||||
**19:23:33.168 UTC**. A separate watchdog automatically sent `SIGCONT` after
|
||||
45 seconds, at **19:24:18.173**. The Board server and Discord Gateway remained
|
||||
running. The proxy was verified running afterward with the same PID and command.
|
||||
|
||||
Added then removed our thumbs-up on the existing admitted Slack message and the
|
||||
completed generation-5 Telegram reply. No provider message, task, bot reaction,
|
||||
credential, callback URL, or endpoint reach setting was changed.
|
||||
|
||||
| Provider | Event | Browser action UTC | Received → processed UTC | Delivery ID |
|
||||
| -------- | ------ | ------------------ | --------------------------- | -------------------------------------- |
|
||||
| Slack | Add | 19:23:39.001 | 19:24:18.420 → 19:24:18.426 | `4453aff3-e004-442b-a340-af44b4e0037f` |
|
||||
| Slack | Remove | 19:23:42.874 | 19:24:18.419 → 19:24:18.424 | `dd006230-f1c1-4195-86b8-3a5a0f364ed9` |
|
||||
| Telegram | Add | 19:23:39.333 | 19:24:18.191 → 19:24:18.197 | `e26e155f-e14f-4fa5-85b1-a291a997df91` |
|
||||
| Telegram | Remove | 19:23:48.281 | 19:24:18.341 → 19:24:18.343 | `2cdc0a84-d8ef-4f85-99df-7d184192c07a` |
|
||||
|
||||
During the pause, at **19:23:57.284**, the proxy was stopped and there were zero
|
||||
new delivery rows. All four events were subsequently processed with null error.
|
||||
At the later check after **19:29 UTC**, there were still exactly four rows, not
|
||||
late duplicate receipts. Counts before any Board sends remained **86 Maya runs,
|
||||
17 tasks, 216 comments, 200 publications**.
|
||||
|
||||
Telegram's already-mounted Activity automatically showed the recovered pair,
|
||||
and the rows were visually inspected. Slack retried **remove before add**.
|
||||
Current Activity is a receipt/processing history, not provider occurrence
|
||||
chronology; it does not persist an occurrence timestamp or reconstruct reaction
|
||||
state. These events do not wake an agent or change task authority. This proves
|
||||
loss-free recovery for this bounded reaction outage, not ordered Slack replay,
|
||||
an exhaustive retry window, or a live Discord Gateway interruption.
|
||||
|
||||
This check did not reconfigure Telegram's webhook URL. The separate historical
|
||||
URL-changing reconnect/backlog proof is recorded in the
|
||||
[Telegram result](./2026-09-05-telegram-live-qualification-result.md).
|
||||
|
||||
## Explicit Board file sends
|
||||
|
||||
Prepared fixtures through the real Board attachment API, not direct database
|
||||
inserts. Each existing linked task received three unbound files: a selected
|
||||
128-byte text document, a selected 2,111,878-byte PNG of the previously used cat,
|
||||
and an unchecked `internal-only.txt`. File prefixes were
|
||||
`board-qa-1930-{provider}-`.
|
||||
|
||||
- Document SHA-256: `fd40030afb62b83181a2a46dde8220e8defecfa0b4328e380c30b1899ccdce24`.
|
||||
- PNG SHA-256: `7693966f6c2b4aaebf9e46359f715fdaede021346bcd926078bb331b1dddc3c1`.
|
||||
|
||||
Started from Telegram connector Activity → Conversations → Open task. Used the
|
||||
actual **Send to channel** composer, selected only the named document and PNG,
|
||||
and explicitly identified the message as a transport test requiring no reply.
|
||||
Continued to the existing Slack, Discord, and GitHub tasks and repeated the same
|
||||
UI action. The unrelated pre-existing Discord attachment stayed unchecked.
|
||||
|
||||
| Provider | Board click UTC | All three publications confirmed UTC | Canonical Board comment |
|
||||
| -------- | --------------- | ------------------------------------ | -------------------------------------- |
|
||||
| Telegram | 19:28:53.440 | 19:28:56.199 | `2ee161b6-3fda-4ff7-b23b-c8f19c2fd087` |
|
||||
| Slack | 19:29:18.770 | 19:29:20.468 | `efc7dd3c-9c7a-46c7-b026-aadb7e3402c2` |
|
||||
| Discord | 19:29:38.549 | 19:29:40.179 | `e32c0140-c16d-43f1-838d-c657f2891bd9` |
|
||||
| GitHub | 19:30:44.104 | 19:30:46.196 | `45cacf74-dc9a-4d0a-abd9-dfef3ce3d73b` |
|
||||
|
||||
Slack and Discord visibly rendered the document's `cobalt otter 47` verification
|
||||
phrase and the cat image. Slack's full image viewer was opened and inspected.
|
||||
Telegram visibly rendered a 128-byte document card and the cat photo. This
|
||||
batch does not claim a downloaded-byte checksum of the provider copies.
|
||||
|
||||
GitHub visibly posted the Board text and two honest private-task file notices;
|
||||
it did not claim to upload bytes or expose a private Board URL. The selected
|
||||
files were available on the Paperclip task after reopening it. GitHub's App
|
||||
transport limitation remains explicit, not a passed native image-upload claim.
|
||||
|
||||
Each send produced exactly one canonical comment and three ordered published
|
||||
rows with provider message IDs and null errors. All eight selected attachments
|
||||
were bound to their respective comment. All four unchecked fixture files stayed
|
||||
unbound and had no publication. Counts became **86 Maya runs, 17 tasks,
|
||||
220 comments, 212 publications**. No additional model run or task was created.
|
||||
|
||||
## Experience findings still requiring a fix/retest
|
||||
|
||||
The provider-side outcomes above passed, but the Board experience needs work:
|
||||
|
||||
1. Slack's send returned **Publishing to channel** with a retained disabled
|
||||
draft even though all three rows subsequently published. The component keeps
|
||||
that returned state without an authoritative refresh. This visit navigated
|
||||
away before measuring an indefinite stale state; a deterministic regression
|
||||
must establish and fix that terminal-refresh gap without replaying the send.
|
||||
2. On GitHub's canonical `CHA-2` task route, the newly sent comment/files did not
|
||||
appear in the mounted timeline after completion. Reopening the task showed
|
||||
them. The banner invalidates UUID-keyed queries while the page can use an
|
||||
issue-identifier key. The same useful outcome must become visible without a
|
||||
reload.
|
||||
|
||||
An independent code audit also found outbound file hydration lacks a bounded
|
||||
storage read and persisted SHA-256 verification. That is failure-injection work,
|
||||
not a corruption observed in these successful live sends. Fixes and supporting
|
||||
tests are being handled separately; none is qualified by the preceding baseline.
|
||||
|
||||
## Follow-up implementation and deterministic verification
|
||||
|
||||
The outbound reader now checks the persisted SHA-256 and exact byte length,
|
||||
bounds storage acquisition and streaming to ten seconds each, and destroys a
|
||||
stream returned after timeout. Task/comment scope and metadata validation run
|
||||
before storage access. Invalid metadata fails definitively; storage/query/read
|
||||
failures remain safe pre-provider retries under the existing five-attempt limit.
|
||||
An accepted provider send with an uncertain durable result still becomes
|
||||
`delivery_unknown`, never an automatic retry.
|
||||
|
||||
The Board composer now uses a scoped read-only batch-status endpoint. It waits
|
||||
for every text/file part, observes explicit Activity resolution, and refreshes
|
||||
both UUID and canonical-identifier task caches. Its exact submitted payload,
|
||||
selected files, and idempotency key are stored before POST in session-scoped
|
||||
browser storage. Reload resumes a known anchor through GET only; a lost response
|
||||
restores a locked draft with an explicit same-key **Retry safely** action.
|
||||
Storage failure before submission prevents an untracked send. State and late
|
||||
responses are isolated by company, task, endpoint, and conversation. This is
|
||||
reload/navigation continuity within that browser session, not a cross-device
|
||||
draft synchronization claim.
|
||||
|
||||
Verification before restarting the live server:
|
||||
|
||||
- Fresh PostgreSQL integration: **269/269**, database
|
||||
`chat_adapters_test_20260907_latency_17` (78.12 seconds).
|
||||
- Focused UI/API/OpenAPI/draft tests: **43/43**; separate hydration/API/OpenAPI
|
||||
subset: **17/17**, including four bounded-read/integrity unit cases.
|
||||
- Five-provider browser file plus Board regressions: **9/9**; clean final Board
|
||||
subset after scope hardening: **4/4** (39.9 seconds).
|
||||
- Shared/server/UI typechecks, UI token gates, and diff checks passed.
|
||||
- The lockfile was unchanged; no broad workspace-test pass is claimed.
|
||||
|
||||
The previous DB14 run passed 268 cases before the final pretransport guard
|
||||
expansion. DB15 exposed metadata validation being masked by missing storage;
|
||||
the guard ordering was corrected, not the expected security result weakened.
|
||||
That run also exposed leaked retry work in a projection-only test fixture. The
|
||||
fixture now retires its exact staged publication and shuts down its service;
|
||||
new hydration tests shut down in `finally`. DB16 passed the new cases but found
|
||||
a timing assumption in a GitHub lease test: a nonblocking HTTP response can
|
||||
precede the worker claim. The test now waits for the same required `processing`
|
||||
state while the lease is held. DB17 is the clean combined result above.
|
||||
|
||||
An early full browser run overlapped development hot reload and missed one
|
||||
success toast; the final clean runs supersede it. The initial red browser test
|
||||
also established that the old component made zero status GETs for eight seconds
|
||||
and kept the completed send disabled.
|
||||
|
||||
The updated-backend live retest below is separate from these deterministic
|
||||
results. A further code audit found synthetic Slack file-share message IDs;
|
||||
reaction matching on uploaded Slack files is not yet qualified.
|
||||
|
||||
## Updated-backend live retest
|
||||
|
||||
Restarted only the isolated Board server as snapshot 11. Health reported loaded
|
||||
`2026.831.0+399.git.43b63da40`, process start **19:52:33.594 UTC**, startup recovery
|
||||
ready, and the Discord Gateway connected. Maya's safe configuration fields were
|
||||
rechecked: `paperclip_runner`, provider `codex`, model `gpt-5.6-luna`. The stored
|
||||
reasoning-effort setting is `low`, but the current native input contract does
|
||||
not propagate that legacy field, as documented in the native-runner report.
|
||||
The following checks do not invoke the model.
|
||||
|
||||
### Slack: paused queue, reload, and automatic completion
|
||||
|
||||
Used the connector Activity **Pause** control, then the canonical `CHA-6` task's
|
||||
**Send to channel** UI. Selected only `board-queue-retest-note.txt` and
|
||||
`board-queue-retest-cat.png`; the previous internal-only fixture stayed unchecked.
|
||||
Clicked Send at **19:53:07.191 UTC** with marker `BOARD-QUEUE-RELOAD-1954`.
|
||||
|
||||
The mounted timeline immediately showed exactly one Board comment and both
|
||||
attachments. The composer truthfully showed **Queued for channel**, **0 of 3
|
||||
parts published**, and a locked draft. Reloading preserved that exact draft and
|
||||
status. A database check while still paused confirmed one comment and three
|
||||
pending rows, not a duplicate submission:
|
||||
|
||||
- Comment: `9e461b1e-ccc9-478b-9799-5fce4c6d96b1`.
|
||||
- Publications: `85a6121d-4554-4c52-89ad-7725b0603329`,
|
||||
`acb97e67-a44a-439b-8828-ad2ab4c95114`, and
|
||||
`ecc5e90f-d5cb-4fcd-b8be-3dda7ffe84f9`.
|
||||
|
||||
Clicked **Resume** at **19:53:35.884**. Text published at **19:53:38.067**, document
|
||||
at **19:53:38.560**, and image at **19:53:39.154**, all with null errors. By the
|
||||
next UI observation at **19:53:42.451**, the same mounted task had automatically
|
||||
closed the draft and re-enabled Send. Slack's actual thread visibly contained
|
||||
the marker text, the document preview with `cobalt otter 47`, and the cat image.
|
||||
There was still one canonical comment. Slack was left active.
|
||||
|
||||
One remaining experience defect was observed and assigned for correction: once
|
||||
the selected attachments bind to the new comment, they disappear from the
|
||||
pending selection list, leaving only the unchecked internal-only file visible.
|
||||
Although the timeline and three-part status are correct, the composer should
|
||||
continue showing the exact locked selected filenames through reload.
|
||||
|
||||
### Discord, Telegram, and GitHub on the same backend
|
||||
|
||||
Uploaded two new unbound fixtures per provider using the Board attachment API,
|
||||
then selected them through each canonical task's actual Send composer. Markers
|
||||
were `BOARD-NEW-BACKEND-{PROVIDER}`. Unrelated and internal-only files stayed
|
||||
unchecked. No agent reply was requested.
|
||||
|
||||
| Provider | Board click UTC | All three parts published UTC | Canonical comment |
|
||||
| -------- | --------------- | ----------------------------- | -------------------------------------- |
|
||||
| Discord | 19:54:40.669 | 19:54:46.478 | `0cbe837f-b48c-42dc-b36f-f2bc7c901ec2` |
|
||||
| Telegram | 19:55:11.630 | 19:55:14.991 | `57639d0a-4c3e-4b2b-80c8-e36c5311fdc6` |
|
||||
| GitHub | 19:55:36.324 | 19:55:38.967 | `0b206e5a-4171-4fb8-b8af-773ad612f419` |
|
||||
|
||||
All nine rows were published with real provider message IDs and null errors.
|
||||
Discord visibly rendered the text preview and cat; Telegram rendered its 128-byte
|
||||
document card and cat photo. The composer closed automatically on each task.
|
||||
GitHub posted the accurate private-file notices. Its already-mounted canonical
|
||||
`CHA-2` timeline now showed the new comment and files without reopening; the cat
|
||||
opened successfully in the private task's full image viewer.
|
||||
|
||||
Final counts were **86 Maya runs, 17 tasks, 224 comments, 224 publications**.
|
||||
All four configured endpoints were active; all four previous internal-only
|
||||
fixtures remained unbound. These checks establish successful transport and Board
|
||||
recovery on the updated backend, not additional model qualification, a throughput
|
||||
SLA, downloaded provider-byte checksums, or native GitHub file upload support.
|
||||
|
||||
### Retained filename receipt and resume latency finding
|
||||
|
||||
UI commit `9763e11fc` fixes the pending-file selection issue. Its filename
|
||||
snapshots remain local to the session's existing scoped send record; they do
|
||||
not change the publication payload or authorize resending bound files. Focused
|
||||
tests passed **46/46**, mocked Board browser cases **4/4**, and UI typecheck,
|
||||
token gates, and diff-check passed.
|
||||
|
||||
Retested live with the unchanged snapshot-11 backend and the refreshed Vite UI.
|
||||
Paused Slack, then sent `BOARD-RECEIPT-CHECK` with `board-receipt-check-note.txt`
|
||||
and `board-receipt-check-cat.png` at **19:58:15.799 UTC**. Before and after reload,
|
||||
the composer showed **Files in this send** with exactly those two names checked
|
||||
and disabled. The canonical timeline also showed the one new comment and both
|
||||
attachments. After eventual completion, a new empty draft offered only the
|
||||
unbound internal-only file, not the files already sent.
|
||||
|
||||
The resume at **19:58:22.029** exposed a separate scheduling defect: the paused
|
||||
head had acquired a synthetic deadline of **19:58:45.898**. Text, document, and
|
||||
image eventually published at **19:58:46.193**, **19:58:46.887**, and
|
||||
**19:58:47.515**, under comment `0eae4850-b752-4f62-bd63-05ff6c27e427`. Slack
|
||||
visibly received all three, but the approximately 25-second post-resume wait is
|
||||
not acceptable transport latency. The scheduling correction and its live retest
|
||||
are separate from the successful filename-persistence result.
|
||||
|
||||
## Subsequent scheduling and Slack identity hardening
|
||||
|
||||
The publication selector now excludes paused/attention endpoints before applying
|
||||
its global page limit. A pause racing an already-selected row restores its
|
||||
original deadline, not a synthetic 30-second delay. Resume therefore makes due
|
||||
work eligible immediately without clearing genuine provider rate-limit or
|
||||
storage-retry deadlines. DB18 reproduced both the old delay and starvation with
|
||||
a one-row page. The revised fixture also resumes through the real configuration
|
||||
service and verifies an unrelated provider backoff remains unchanged. DB19
|
||||
exposed incomplete fixture inventory during provider revalidation; the test now
|
||||
returns its actually available channel rather than bypassing the reach check.
|
||||
|
||||
The pinned Slack adapter now uses the uploaded file's real share timestamp for
|
||||
its exact channel/thread. Sparse upload responses use a bounded, read-only
|
||||
`files.info` lookup under the existing required `files:read` scope. Every returned
|
||||
file must match its expected uploaded ID and have one unambiguous common share
|
||||
timestamp. Missing, mismatched, timed-out, or ambiguous identities after upload
|
||||
remain `delivery_unknown`, not synthetic success or a retry that uploads again.
|
||||
An unpreparable local file fails definitively before transport. Adapter and
|
||||
bounded-hydration units passed **49/49** after the final patch; server typecheck
|
||||
passed. Applying the tracked patch to pristine 4.39.0 reproduced the installed
|
||||
adapter bytes exactly. The lockfile remains unchanged as instructed; a fresh
|
||||
frozen-lockfile install was not part of this check.
|
||||
|
||||
DB20 passed **268/269**, including the new resume and exact Slack file-ID tests.
|
||||
Its failure was an existing slash-command test that raced provider-root
|
||||
completion against channel-access revocation and assumed a task must result.
|
||||
The recorded delivery was correctly filtered because the destination was
|
||||
disabled. The test now explicitly controls transport and admission scheduling,
|
||||
retains the duplicate-acknowledgement and lease assertions, commits revocation,
|
||||
then drains the exact receipt and requires denial with no task or wake. No
|
||||
production authorization check was relaxed to make that expectation pass.
|
||||
|
||||
The clean combined DB21 rerun passed **269/269**. The isolated deterministic
|
||||
revocation test also passed on its own newly migrated database; server typecheck
|
||||
passed after the final test changes. Live verification of the new Slack identity
|
||||
and scheduling behavior follows separately.
|
||||
|
||||
The separate early-reaction race remains open: a reaction arriving before the
|
||||
outbound message link commits currently has no exact lineage and is dropped.
|
||||
Resolving real Slack file IDs fixes normal post-commit matching, not that race.
|
||||
|
||||
## Snapshot 12: fast resume passes; Slack share visibility exposes a failure
|
||||
|
||||
Loaded snapshot 12, `2026.831.0+402.git.dc1d17351`, at **20:09:56.742 UTC**;
|
||||
health/recovery and Discord Gateway were ready. Paused Slack and sent
|
||||
`SLACK-UPLOAD-ID-CHECK` at **20:10:56.530** with `slack-upload-id-note.txt` and
|
||||
`slack-upload-id-cat.png`. Reload retained both checked, disabled filenames.
|
||||
While paused, all three publications had zero attempts and null retry deadlines.
|
||||
|
||||
Resumed at **20:11:15.347**. The text published at **20:11:16.915**, a 1.568-second
|
||||
resume-to-acknowledgement sample, without the prior synthetic delay. However,
|
||||
the document then entered `delivery_unknown` at **20:11:17.645** because the
|
||||
one-shot file metadata lookup could not resolve its share. The image remained
|
||||
pending behind that uncertain result. The Board truthfully showed **Delivery
|
||||
not confirmed**, **1 of 3 parts published**, and kept its exact draft.
|
||||
|
||||
Slack visibly contained the document and correct `cobalt otter 47` content.
|
||||
Its native permalink timestamp was `1788811877.783349`, corresponding to
|
||||
**20:11:17.783**: the actual share appeared about 138 ms after the adapter had
|
||||
given up. This is failed file-identity qualification and evidence of eventual
|
||||
share visibility, not a successful automatic file receipt. No upload retry was
|
||||
performed. The follow-up uses bounded read-only polling for the same uploaded
|
||||
file IDs, keeping the original upload and ambiguity safeguards unchanged.
|
||||
|
||||
- Canonical comment: `1181fcde-c098-4136-a2c9-e3dd13c6dd0c`.
|
||||
- Text: `12b3bef4-390f-4e89-9dcf-cb930aa52f13`, real ID `1788811876.864209`.
|
||||
- Held document: `ad9e2afb-85ab-4ed6-90bb-5168f760688a`.
|
||||
- Pending image: `24d08c07-63de-45af-9f4b-8d1a8b00342b`.
|
||||
|
||||
The current operator **Mark delivered** action records an audited confirmation
|
||||
but does not accept a recovered provider message ID or reconstruct its message
|
||||
link. An operator-resolved document therefore must not be counted as a passed
|
||||
automatic lineage/reaction test; a fresh normally acknowledged file is needed.
|
||||
|
||||
The follow-up adapter change polls sparse, matching `files.info` results under
|
||||
one absolute five-second deadline, with paced 100/250/500/1000 ms waits. It never
|
||||
uploads again and cannot start a lookup after a delayed token resolution has
|
||||
exhausted that deadline. Missing/mismatched identities and lookup errors still
|
||||
produce a safe uncertain-delivery result. Focused adapter and hydration tests
|
||||
passed **51/51**; the first typecheck caught a generic mock typing error in the
|
||||
new late-token test. The corrected test and server typecheck pass. Live qualification of
|
||||
this polling change is recorded below rather than inferred from those tests.
|
||||
|
||||
## Snapshot 13: real file identities and file reactions pass
|
||||
|
||||
Loaded `2026.831.0+403.git.dddcf0d93` at **20:21:08.692 UTC**, with startup
|
||||
recovery ready and Discord Gateway connected. Rechecked the existing document
|
||||
in Slack, including its correct fixture text, then used Activity's **Mark
|
||||
delivered** at **20:21:31.363**. It retained one upload attempt; no retry or
|
||||
provider-ID backfill was performed. Its missing automatic lineage remains an
|
||||
explicit limitation of manual resolution, not a successful identity test.
|
||||
|
||||
The previously pending image then published once, automatically, at
|
||||
**20:21:36.719**, with real Slack ID `1788812496.261909` and a matching outbound
|
||||
message link. Slack showed the cat image in the intended thread. The mounted
|
||||
Board composer closed automatically after the batch completed. A new draft
|
||||
offered the new unbound note and the internal-only fixture, not already sent
|
||||
files.
|
||||
|
||||
Sent `SLACK-FILE-ID-RECHECK` from the Board at **20:21:55.713**, selecting only
|
||||
`slack-upload-id-recheck-note.txt`. Text and document published in one attempt
|
||||
each under canonical comment `f1118339-a6e2-40f5-b4e6-b67fe3883e1f`. The document
|
||||
publication `e7517c83-15b2-4928-964b-422d1b64e8d1` completed at
|
||||
**20:21:56.890**, with native ID `1788812516.721189` and a matching outbound link.
|
||||
Slack visibly rendered the correct `cobalt otter 47` content. No manual
|
||||
resolution or duplicate send was needed, and the Board draft cleared again.
|
||||
|
||||
Added and removed the operator's thumbs-up on that exact document, then on the
|
||||
new image, through Slack's message controls. All four receipts processed once,
|
||||
without error, and the Activity tab refreshed to show them:
|
||||
|
||||
| File | Event | Received UTC | Processed UTC | Exact native message ID |
|
||||
| ----- | ------- | ------------ | ------------- | ----------------------- |
|
||||
| Note | added | 20:22:38.257 | 20:22:38.262 | `1788812516.721189` |
|
||||
| Note | removed | 20:22:56.917 | 20:22:56.921 | `1788812516.721189` |
|
||||
| Image | added | 20:23:24.762 | 20:23:24.768 | `1788812496.261909` |
|
||||
| Image | removed | 20:23:27.958 | 20:23:27.962 | `1788812496.261909` |
|
||||
|
||||
Both test reactions were removed; existing reactions were untouched. Maya's
|
||||
run counts remained 78 succeeded / 8 failed, with no running or queued run.
|
||||
An unrelated automatic productivity-review task, CHA-18, appeared during this
|
||||
window; it has no chat conversation and must not be attributed to these
|
||||
reactions. All four configured endpoints remained active.
|
||||
|
||||
Functional result: fresh Slack document/image delivery, exact outbound lineage,
|
||||
post-commit file reactions, and automatic Board draft completion passed live.
|
||||
The one-shot failure did require operator recovery; the corrected fresh-send
|
||||
journey did not. This does not qualify the still-open reaction-before-link race,
|
||||
native model generation under exhausted quota, or Microsoft Teams.
|
||||
|
||||
### Manual confirmation is not a recovered provider receipt
|
||||
|
||||
Read-only review confirmed that `mark_delivered` intentionally records the
|
||||
operator's confirmation without inventing an external ID. Exact reactions or
|
||||
later message replacement cannot use a link that does not exist. The current
|
||||
adapter discards its known uploaded file IDs when bounded share lookup expires,
|
||||
so old uncertain/manual-confirmed rows cannot safely be matched later by
|
||||
filename, text, or time-window searches.
|
||||
|
||||
A future recovery path would need a durable internal partial receipt from the
|
||||
original attempt: the exact server-observed file IDs, publication/attempt, bot
|
||||
identity, and intended channel/thread. It could then repeat only scoped,
|
||||
read-only metadata lookups under current authorization, require the same unique
|
||||
share match, and transactionally bind the identity without another upload or
|
||||
repeating manual-completion side effects. That path is not implemented or
|
||||
claimed in this qualification.
|
||||
|
||||
## Early-reaction recovery hardening
|
||||
|
||||
The previously open reaction-before-link race now has a durable, bounded path.
|
||||
If the exact message link is not yet visible, only a currently authorized
|
||||
destination with an unambiguous in-flight publication can stage a minimal
|
||||
reaction receipt. It has no conversation/task association until the exact
|
||||
outbound message link exists. Recovery rechecks the endpoint/runtime fence,
|
||||
destination reach, and current principal authorization; it never creates a task,
|
||||
comment, run, or wake. Unknown unrelated messages are not admitted just because
|
||||
they share a channel.
|
||||
|
||||
Pending reactions stay outside both ordinary inbound FIFO selectors. Their
|
||||
metadata-only recovery runs alongside ordinary deliveries, with paced retries
|
||||
bounded by 20 attempts and two minutes. Exact provider-event deduplication is
|
||||
preserved across the original callback, retry, and server restart. A completed
|
||||
DM generation can own its late reaction; a newer generation is never guessed.
|
||||
|
||||
Independent review found and corrected three subtle interleavings: publication
|
||||
commit between the unlocked preflight reads; conversation FK key-share locks
|
||||
deadlocking with endpoint-first reaction admission; and a pre-lock timestamp
|
||||
allowing replay after expiry. Conversation locks now use `NO KEY UPDATE`, and
|
||||
expiry is evaluated after acquiring the delivery lock. Recovery promises are
|
||||
observed immediately and joined even if ordinary delivery processing throws,
|
||||
before the original error is rethrown.
|
||||
|
||||
Seven focused real-PostgreSQL cases passed on fresh `reaction_focus_01` (5/5)
|
||||
and `reaction_focus_02` (2/2): preflight recheck, durable duplicate/restart replay
|
||||
without task work, publication-link lock overlap, late-duplicate expiry,
|
||||
post-lock clock expiry, revoked destination, and completed older-DM ownership.
|
||||
Server, shared, and UI typechecks passed. Cross-endpoint liveness under a held
|
||||
reaction lock was code-reviewed, not a separately executed eighth fixture.
|
||||
|
||||
The full combined suite then passed **276/276** on the fresh migrated
|
||||
`chat_adapters_test_20260907_reaction_full_01` database, in 72.10 seconds
|
||||
(64.52 seconds of tests). This run included the final frozen service/test files
|
||||
and all seven additions. Simulated provider failures in its log are intentional
|
||||
negative fixtures, not live-provider failures.
|
||||
|
||||
### Recovery failure-path regression follow-up
|
||||
|
||||
Two additional real-PostgreSQL fixtures now exercise ordinary inbound-drain
|
||||
failure while both action and reaction recovery are in flight. Each injects an
|
||||
error only at the fixture endpoint's inbound lease acquisition. One releases
|
||||
action recovery first; the other releases reaction recovery first. Both require
|
||||
the sweep to remain pending until the second recovery finishes, then reject
|
||||
with the exact original error. Durable reaction/action state completes once,
|
||||
without extra comments, tasks, runs, wakes, or publication sends; the ordinary
|
||||
delivery remains unprocessed and recovery leases are released.
|
||||
|
||||
The focused run passed **2/2** on fresh
|
||||
`chat_adapters_test_20260907_reaction_join_01`; server TypeScript passed.
|
||||
The isolated mocked browser suite also passed **9/9** in 2.7 minutes, covering
|
||||
all five setup/management journeys and four Board batch-delivery/reload cases.
|
||||
Those browser cases use a throwaway instance on port 3199 and mocked providers,
|
||||
not the signed-in live provider sessions or the live instance on port 3103.
|
||||
|
||||
The combined suite then passed **278/278** on fresh migrated database
|
||||
`chat_adapters_test_20260907_reaction_full_02`, in 79.68 seconds (71.62 seconds
|
||||
of tests). This includes both recovery-release orders and the prior seven
|
||||
reaction-link regressions. The current change is test-only; it does not add a
|
||||
new live-provider qualification or change the running server's production code.
|
||||
|
||||
## Snapshot 14: deployed; post-restart browser smoke remains unverified
|
||||
|
||||
Loaded `2026.831.0+407.git.e6f52b4cc` at **20:44:52.362 UTC**. Health and
|
||||
startup recovery are ready; Discord Gateway connected. All four configured
|
||||
endpoints remain active. A read-only recheck confirms Maya still uses
|
||||
`paperclip_runner` / `codex` / `gpt-5.6-luna`; the four earlier successful text
|
||||
run rows retain `native` / `codex_app_server`. No model defaults were changed.
|
||||
|
||||
The attempted live post-restart reaction smoke did not complete. Browser click
|
||||
and scroll calls returned without a visible effect in Slack and Paperclip,
|
||||
including a newly opened Board catalog tab. One browser-automation session reset
|
||||
and the documented alternate interaction API did not restore input. Navigation,
|
||||
rendered snapshots, and screenshots remained available. No new Slack reaction
|
||||
receipt arrived after this restart, and Maya's run counts remained 78 succeeded
|
||||
and 8 failed, with no queued or running run. No duplicate message or credential
|
||||
rotation was attempted as a workaround.
|
||||
|
||||
The new early-reaction path therefore has the database/integration coverage
|
||||
above, but no passed post-deployment live reaction smoke. Snapshot 13's actual
|
||||
document/image and reaction results remain valid evidence for that version;
|
||||
they are not relabeled as snapshot 14 results. Browser-input recovery is a
|
||||
testing-tool limitation, not an established Slack or Paperclip product defect.
|
||||
Model-driven follow-ups still require restored Codex capacity; Teams still
|
||||
requires the eligible tenant/admin setup. The isolated server is left running,
|
||||
with the public webhook-only proxy and private Board boundary unchanged.
|
||||
|
||||
## Frozen-install release gate
|
||||
|
||||
The preserved lockfile is now a confirmed release blocker, not merely an
|
||||
unexecuted check. On September 7, the non-regenerating diagnostic
|
||||
`pnpm install --frozen-lockfile --lockfile-only --ignore-scripts --offline`
|
||||
exited with `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH`: the current overrides do not
|
||||
match the lockfile. It stopped before validating dependency and patch entries;
|
||||
source inspection also shows that the five pinned chat-adapter dependencies
|
||||
and their patches are absent from that lockfile. The diagnostic left the
|
||||
lockfile and working tree unchanged and did not replace the live server's
|
||||
installed modules.
|
||||
|
||||
The installed, patched dependency tree used for the recorded tests is therefore
|
||||
not proof of a reproducible frozen installation from this branch. The existing
|
||||
instruction not to edit or commit `pnpm-lock.yaml` remains in force. No patch,
|
||||
override, or dependency was removed to make the check appear green. Release
|
||||
qualification needs a reconciled lockfile and a clean frozen-install retest
|
||||
after that constraint is resolved; the active local server is unaffected.
|
||||
|
||||
## Upstream reconciliation remains open
|
||||
|
||||
A fresh fetch on September 7 found `origin/master` at `d8b958053`, four commits
|
||||
ahead of this branch's merge base `f6a211479`. In addition to the lock refresh,
|
||||
upstream adds guarded Runner API fallback, responsible-person GitHub execution
|
||||
identity, and recent-task ordering. The tested checkpoint `9007e4111` does not
|
||||
contain those changes.
|
||||
|
||||
A non-worktree `git merge-tree` diagnostic confirmed conflicts in migration
|
||||
metadata 0240–0245 and the journal, the OpenAPI route test, issue routes, and
|
||||
native runner tool authority. Automatically merged heartbeat/executor paths
|
||||
still require semantic verification; a textual auto-merge is not proof that
|
||||
native chat authority and continuation behavior remain correct. No merge,
|
||||
rebase, migration rewrite, or lockfile update was applied to the live worktree.
|
||||
The existing live database must retain its applied migration history during
|
||||
that future reconciliation. Current-source release qualification cannot be
|
||||
claimed against the newer upstream revision until this work and its tests are
|
||||
complete.
|
||||
|
||||
Independent review identified the concrete merged checks: retain both the
|
||||
chat-specific native tool/attachment authority and upstream's guarded API
|
||||
fallback; carry identity-context fields through the rewritten issue handlers;
|
||||
test fresh and already-migrated databases; and verify broker-bound resumed
|
||||
turns with different linked actors. Guest messages are currently quarantined,
|
||||
and higher-trust runs omit their bodies and attachments. Upstream identity
|
||||
initialization skips authorless comments and may inherit a continuation actor,
|
||||
so guest-root and linked-A/guest/linked-B scenarios need explicit combined
|
||||
identity/credential tests. This is an unverified integration boundary, not
|
||||
evidence that credentials leaked in the tested branch.
|
||||
|
||||
## Slack accepted-upload receipt recovery
|
||||
|
||||
The bounded share lookup still had a process-interruption gap: after Slack
|
||||
accepted a file, Paperclip could lose the returned file IDs before confirming
|
||||
the share's real message timestamp. The follow-up records those exact IDs in
|
||||
a private, attempt-bound `slack_file_upload_receipt` action immediately after
|
||||
the successful upload response, before the eventual-consistency lookup. It
|
||||
uses a per-call asynchronous context around ordinary `Thread.post`, preserving
|
||||
the SDK's sent-message, typing, and history behavior.
|
||||
|
||||
An independent recovery lane performs only metadata reads for the saved file
|
||||
IDs. It does not re-upload files, guess timestamps from filenames, or create
|
||||
another model turn. Settlement requires the exact publication attempt,
|
||||
endpoint bot/runtime/credential identity, conversation, channel/thread, and
|
||||
current destination reach. It is endpoint-authorized bookkeeping for bytes
|
||||
already accepted, not a newly authorized external-user send; file publications
|
||||
have no original-principal anchor, and this change does not claim to add one.
|
||||
Task controls and interactive cards are excluded. Historical attachment reuse
|
||||
continues to authorize its own requesting principal separately.
|
||||
|
||||
An exact receipt can settle an unconfirmed publication automatically. After
|
||||
an operator explicitly marks that same attempt delivered, recovery may only
|
||||
enrich the missing provider identity/link; it must not repeat completion
|
||||
effects or alter the confirmed timestamp. Retry/cancel/new-attempt changes
|
||||
invalidate the old receipt. A conflicting existing message binding remains
|
||||
unconfirmed. Receipts are omitted from normal endpoint Activity and publication
|
||||
payloads. Older uploads without a durable receipt cannot be reconstructed by
|
||||
this change.
|
||||
|
||||
Independent review caught two worker races before qualification: stale
|
||||
selection could bypass a newly scheduled backoff, and held endpoints could
|
||||
monopolize the bounded selection page. Claims now recheck eligibility and
|
||||
attempts under the row lock; held endpoints and same-attempt streaming work
|
||||
are excluded before the page limit.
|
||||
|
||||
The first real PostgreSQL run caught an additional timestamp-precision defect:
|
||||
a server-default `updated_at` had microseconds, but the decoded JavaScript
|
||||
timestamp used for equality had only milliseconds. The receipt remained
|
||||
`received` and recovery returned zero. This is a production claim-path defect,
|
||||
not a flaky timing assertion. Claims now use the already-locked row; malformed
|
||||
or removed-endpoint receipts use a precision-safe state and semantic JSONB
|
||||
comparison, with SQL null distinguished from JSONB null. The manual-confirmation
|
||||
case also exposed untyped `jsonb_build_object` parameters; explicit casts fix
|
||||
the PostgreSQL error before any deployment.
|
||||
|
||||
Supporting verification so far:
|
||||
|
||||
- All pinned-provider adapter and reconciliation-coordinator tests passed
|
||||
**59/59**. Coverage includes reverse-order concurrent upload callbacks,
|
||||
callback failure without a second upload, strict accepted-ID validation,
|
||||
preserved SDK sent-message methods, independent reconciliation, and joined
|
||||
shutdown for both successful and failed receipt lookups.
|
||||
- The frozen tracked patch applied cleanly to pristine Slack adapter 4.39.0.
|
||||
Its output exactly matches the installed module, SHA-256
|
||||
`094eafb219f99546c5189a28e6c25b228034cc6a589edca63ed09a09d7ca42ea`.
|
||||
The lockfile was not modified; this is patch reproducibility, not a passed
|
||||
frozen workspace installation.
|
||||
- Fresh databases `chat_adapters_test_20260907_slack_receipt_01` and `_02`
|
||||
exposed the timestamp and manual-confirmation SQL defects. After fixes,
|
||||
`_03` passed both expanded database cases, including duplicate receipt
|
||||
capture, a 25-row paused backlog, exact-message conflict, cancellation, and
|
||||
identity-only manual-confirmation enrichment.
|
||||
- Fresh `_04` passed **4/4** focused cases, adding two workers demonstrably
|
||||
preselected behind a held credential lease, and channel reach revoked during
|
||||
a held metadata lookup. Only one competing lookup ran, its new retry deadline
|
||||
remained intact, and revoked reach produced neither a provider link nor a
|
||||
second upload. The final malformed-row SQL-null variant landed afterward
|
||||
and was included in the final full-suite gate below.
|
||||
- The first full receipt suite passed 278/282. Its fake Slack transport wrongly
|
||||
invoked upload acceptance before a definite-rejection hook, leaving a receipt
|
||||
that also disrupted two later tests. The fixture now separates pre-acceptance
|
||||
rejection from post-acceptance ambiguity; production acceptance handling was
|
||||
not weakened. An unrelated Discord Gateway renewal also consumed a global
|
||||
one-shot database fault intended for the Slack lifecycle test. That fault is
|
||||
now bound to the exact lifecycle transaction's uncommitted terminal row,
|
||||
proving rollback of both its comment and terminal state. Fresh `_05` passed
|
||||
all **6/6** affected cases, including the final null-result receipt variant.
|
||||
- Final fresh database
|
||||
`chat_adapters_test_20260907_slack_receipt_full_02` passed **282/282** in
|
||||
63.91 seconds (58.15 seconds of tests). Server TypeScript passed after the
|
||||
final code and fixture changes. These are simulated-provider tests with
|
||||
real PostgreSQL, not new live-provider or process-kill qualification. The
|
||||
earlier frozen-install, upstream, browser-input, model-capacity, and Teams
|
||||
gates remain open; this is not a whole-product readiness sign-off.
|
||||
|
||||
## Snapshot 15: receipt repair deployed; live ambiguity proof still open
|
||||
|
||||
Committed and pushed `9277e0dc5`. The isolated instance restarted with loaded
|
||||
version `2026.831.0+411.git.9277e0dc5`; startup recovery was ready at
|
||||
**21:36:00.332 UTC**. The health endpoint and `/CHA/apps` both returned 200,
|
||||
and Discord Gateway connected. Slack, GitHub, Discord, and Telegram endpoints
|
||||
remain active. Maya still uses `paperclip_runner` / `codex` /
|
||||
`gpt-5.6-luna`; no global defaults or agent model settings changed. There were
|
||||
no active Maya runs at restart. The webhook-only proxy stayed running on 3104,
|
||||
and the Board remains private on 3103.
|
||||
|
||||
The live rare-path test—Slack accepts bytes, share identity is temporarily
|
||||
unavailable, and durable metadata recovery later binds the real message—is
|
||||
still unqualified. Snapshot 13's fast-path file evidence is not relabeled as
|
||||
this new recovery-path evidence. Browser-input recovery remains unresolved,
|
||||
and a fresh account-limit check still reports exhausted weekly Codex capacity
|
||||
with no reset credit. No model-driven retry, historical-file resend, or Teams
|
||||
live pass is claimed by this deployment. The final automated evidence for the
|
||||
deployed source remains 282/282 database cases, 59/59 adapter/coordinator cases,
|
||||
server TypeScript, and exact pinned-patch reproduction.
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
# Native chat live reach audit — 2026-09-07
|
||||
|
||||
## Scope
|
||||
|
||||
Server `074271e3fc4c2419c8894b7a564916ff54b90e32`, isolated Board
|
||||
`http://127.0.0.1:3103`, company Chat Adapter E2E. Maya E2E remains
|
||||
`paperclip_runner`, provider `codex`, model `gpt-5.6-luna`. The server's startup
|
||||
recovery was ready before this exercise. Slack, GitHub, Discord, and Telegram
|
||||
were active; Teams was not configured.
|
||||
|
||||
This is a live negative reach test, not a new model-response benchmark. The
|
||||
Codex account was already returning `usageLimitExceeded`. No model-starting
|
||||
prompts were sent while destinations were enabled, and no historical failed
|
||||
run was rewritten or replayed to manufacture a successful result.
|
||||
|
||||
## Journey
|
||||
|
||||
Starting from Connectors → Browse → Manage, the linked Board operator disabled
|
||||
only the existing authorized test destination in Settings, sent one message in
|
||||
the provider's existing test conversation through the signed-in in-app browser,
|
||||
and inspected Paperclip Activity and the provider. Returning to Settings proved
|
||||
the disabled state persisted; the original setting was then restored.
|
||||
|
||||
| Provider | Disabled setting | Send time (UTC) | Observed result |
|
||||
| -------- | ---------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Slack | `#pc-chat-live-0905b` | 17:56:10.018 | Activity: filtered, “Destination is not enabled in Paperclip”; no reaction/reply |
|
||||
| GitHub | `cryppadotta/paperclip-chat-e2e-enabled` | 18:01:08.615 | Saved comment persisted after reload; signed webhook acknowledged at 18:01:10; content rejected before durable ingress; no reaction/reply |
|
||||
| Discord | Clawd `#general` | 18:02:53.827 | Activity: filtered, same destination explanation; no reaction/reply |
|
||||
| Telegram | Allow direct messages | 18:03:55.816 | Activity: filtered, same destination explanation; no reply |
|
||||
|
||||
Provider markers were `SLACK-REACH-DISABLED-0907-1256`,
|
||||
`GITHUB-REACH-DISABLED-0907-1301`, `DISCORD-REACH-DISABLED-0907-1304`, and
|
||||
`TELEGRAM-REACH-DISABLED-0907-1305`. The suffix is a unique test label, not a
|
||||
precise send-time claim. GitHub's comment is
|
||||
[issuecomment-5574211696](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/issues/2#issuecomment-5574211696).
|
||||
|
||||
## Durable cross-checks
|
||||
|
||||
| Provider | Delivery ID | Received → processed (UTC) | Normalized event retains marker? |
|
||||
| -------- | -------------------------------------- | --------------------------- | -------------------------------- |
|
||||
| Slack | `e80087f8-7a49-44bf-8ad0-cd45aa670fc1` | 17:56:10.579 → 17:56:11.340 | No |
|
||||
| Discord | `3d520054-5e5b-4d40-b094-db0c51eba189` | 18:02:54.057 → 18:02:54.815 | No |
|
||||
| Telegram | `9bb5dc04-9c17-4a8b-a458-306bbdd04d00` | 18:03:57.048 → 18:03:57.049 | No |
|
||||
|
||||
GitHub differs intentionally: `stageGitHubWebhookIngress` authenticates the
|
||||
request, then checks repository enablement before storing the signed body.
|
||||
There was no new ingress action or delivery row. Its unchanged Activity is
|
||||
therefore not itself proof that a callback arrived; the server's HTTP 200 log,
|
||||
persisted provider comment, disabled resource, and source-level admission gate
|
||||
provide the cross-check. No raw webhook body was inspected or retained as
|
||||
evidence.
|
||||
|
||||
From the 17:55:49.711 baseline through the final 18:05:41.790 read:
|
||||
|
||||
- Maya's total run count remained **86**.
|
||||
- The company had **zero** new tasks, internal comments, or publications.
|
||||
- All four endpoints were active, with direct-message settings restored true.
|
||||
- Slack's original private test channel, Discord `#general`, and GitHub's
|
||||
`paperclip-chat-e2e-enabled` repository were restored enabled.
|
||||
- GitHub's separate `paperclip-chat-e2e-disabled` repository stayed disabled;
|
||||
no other Discord channel was enabled.
|
||||
|
||||
## Experience findings
|
||||
|
||||
Functional outcome: existing-conversation reach revocation worked in these four
|
||||
live cases. It did not wake the native agent, retain refused message text, or
|
||||
publish externally. The provider test markers are intentionally retained in
|
||||
the disposable test conversations.
|
||||
|
||||
Activity originally displayed only “Sep 7, 2026” for every event. During this
|
||||
exercise it was impossible to distinguish same-day deliveries, queue updates,
|
||||
and retries from their visible time. The follow-up UI change uses the shared
|
||||
date-time formatter with seconds, semantic `time` elements, and the exact
|
||||
server timestamp on hover. The updated Telegram Activity was visually checked
|
||||
in the running Board: the rejected event reads “Sep 7, 2026, 1:03:57 PM” and the
|
||||
list remains readable without clipping at the observed desktop viewport.
|
||||
|
||||
Supporting checks for that UI change: focused date formatting and chat UI
|
||||
contracts **29/29**; deterministic five-provider browser suite **5/5**, including
|
||||
second-level display and exact timestamp attributes; UI TypeScript and token
|
||||
gates passed. The deterministic suite uses mocked chat-provider endpoints and
|
||||
does not count as live provider or native model evidence.
|
||||
|
||||
This is only the negative, existing-conversation portion of runbook C2. A fresh
|
||||
message after re-enabling, fresh-task admission, access races during active
|
||||
model execution, and post-quota recovery are not qualified by this exercise.
|
||||
The final-source multi-provider release gate remains open.
|
||||
|
||||
## Follow-up: inspectable GitHub rejection
|
||||
|
||||
The initial GitHub result above exposed a diagnostic gap: an operator could
|
||||
not distinguish an authenticated but disabled destination from a missing
|
||||
webhook. The follow-up backend change records a content-free, non-replayable
|
||||
filtered Activity receipt for a known disabled repository after signature,
|
||||
installation, and endpoint checks. The delivery ID is hashed; no comment text,
|
||||
author, conversation, or webhook body is retained. Unknown repositories and
|
||||
invalid signatures still do not create this receipt.
|
||||
|
||||
A new regression reproduced the missing receipt before the change. After the
|
||||
fix, the full chat integration suite passed **265/265** on fresh database
|
||||
`chat_adapters_test_20260907_latency_09`. The added case covers invalid
|
||||
signatures, three concurrent identical deliveries producing one receipt,
|
||||
metadata-only Activity, no agent wake or ingress action, and no replay when
|
||||
the repository is re-enabled.
|
||||
|
||||
### Live follow-up result
|
||||
|
||||
Restarted the isolated server at commit
|
||||
`639bf1a20af9ca9afaecae126c12b7add714f19c`, with startup recovery ready before
|
||||
the test. From Connectors → Browse → Manage GitHub → Settings, disabled only
|
||||
`paperclip-chat-e2e-enabled`, then sent `GITHUB-REACH-RECEIPT-0907-1325` at
|
||||
**18:24:59.455 UTC** in the same live test issue. The comment persisted after
|
||||
navigating out to the repository's issue list and reopening the issue:
|
||||
[issuecomment-5574403287](https://github.com/cryppadotta/paperclip-chat-e2e-enabled/issues/2#issuecomment-5574403287).
|
||||
|
||||
Paperclip Activity showed “message ignored”, “Destination is not enabled in
|
||||
Paperclip”, and **Sep 7, 2026, 1:25:02 PM** (local time). The rendered row was
|
||||
readable with no clipping at the observed desktop viewport. The initial
|
||||
Activity visit preceded the new receipt appearing; revisiting the tab showed
|
||||
it. This does not establish instantaneous live refresh or all transition
|
||||
timings.
|
||||
|
||||
Delivery `fdec8621-2423-45b1-8349-83666a30f48e` was received at
|
||||
**18:25:02.157 UTC** and processed at **18:25:02.158 UTC**. It had filtered
|
||||
state, null conversation/principal, and only the hashed provider event ID,
|
||||
event kind, disabled-resource ID, and content-retention-false reason. There
|
||||
was no retained message text and no new GitHub ingress action.
|
||||
|
||||
From baseline **18:24:49.768** through **18:26:00.895 UTC**, counts remained
|
||||
Maya runs **86**, company tasks **17**, internal comments **216**, and
|
||||
publications **200**. Restored the enabled repository; the separate disabled
|
||||
repository stayed off. Maya's persisted configuration remained
|
||||
`paperclip_runner` → `codex` → `gpt-5.6-luna`.
|
||||
|
||||
Functional outcome: the original missing-receipt symptom is fixed in this
|
||||
live case without admitting refused work or retaining its content. Experience
|
||||
quality: this diagnostic path is now understandable from the Board; the
|
||||
broader model-driven and Teams release gaps remain open.
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
# Live native chat reaction audit — 2026-09-07
|
||||
|
||||
## Environment and journey
|
||||
|
||||
Isolated Board `http://127.0.0.1:3103`, company Chat Adapter E2E, native Maya
|
||||
E2E (`paperclip_runner` → `codex` → `gpt-5.6-luna`). The running backend was
|
||||
`639bf1a20`; the updated UI was served through the development middleware.
|
||||
These tests do not start model turns and do not qualify model quota recovery.
|
||||
|
||||
Using the signed-in in-app browser, added then removed only our thumbs-up
|
||||
reaction on existing admitted human test messages in Slack's private
|
||||
`pc-chat-live-0905b` thread and Discord's Clawd test thread. Inspected provider
|
||||
state, connector Activity, and durable delivery metadata. No credentials,
|
||||
message bodies, or model reasoning were copied into this evidence.
|
||||
|
||||
## First-cycle result
|
||||
|
||||
| Provider | Event | Browser action UTC | Received → processed UTC | Delivery ID |
|
||||
| -------- | ------ | ------------------ | --------------------------- | -------------------------------------- |
|
||||
| Slack | Add | 18:27:38.318 | 18:27:39.413 → 18:27:39.415 | `7d36d051-02e2-49b1-b53a-518bfc432403` |
|
||||
| Slack | Remove | 18:28:12.516 | 18:28:13.527 → 18:28:13.529 | `e0595765-cb21-4aee-b699-6a13108409df` |
|
||||
| Discord | Add | 18:27:43.071 | 18:27:43.593 → 18:27:43.594 | `d39b9424-8270-437a-91b8-1a39a4447437` |
|
||||
| Discord | Remove | 18:28:18.539 | 18:28:18.818 → 18:28:18.821 | `015aceb0-c6b3-459f-a39a-81bb6902d97c` |
|
||||
|
||||
All four were processed, bound to the existing conversation, and had no error.
|
||||
From **18:27:28.160** through **18:29:52.686 UTC**, counts remained Maya runs
|
||||
**86**, company tasks **17**, internal comments **216**, and publications
|
||||
**200**. Reactions were not interpreted as a message, answer, or authority.
|
||||
The test reactions were removed; the bot's existing eyes reactions were not
|
||||
changed.
|
||||
|
||||
## Activity refresh defect and fix
|
||||
|
||||
The initial live visit could show the earlier addition even after the removal
|
||||
was durably processed. Chat detail queries inherited the global 30-second
|
||||
fresh cache and had no periodic refresh; callbacks do not necessarily emit a
|
||||
Board activity invalidation. The deterministic Slack browser regression failed
|
||||
before the fix: a new fixture event never appeared within eight seconds while
|
||||
Activity remained mounted.
|
||||
|
||||
Commit `2a554ce22` refreshes mounted Activity and Conversations queries and
|
||||
their endpoint health every five seconds, with background polling disabled.
|
||||
Freshness is zero on those operational queries so reopening a view also checks
|
||||
current state. No new setting, visual token, or provider request was added.
|
||||
|
||||
The five-provider deterministic browser suite passed **5/5**, including
|
||||
conversation-state, new-activity, and endpoint pause/resume changes without
|
||||
reload or tab navigation. UI contracts passed **26/26**; UI TypeScript and token
|
||||
gates passed. Those provider responses are mocked, separate from the live
|
||||
evidence here. An intervening test run failed on an incorrect capitalized
|
||||
`Waiting` selector; the actual existing badge text is `waiting`.
|
||||
|
||||
Live Slack retest used a fresh Board Activity view (tab 55) and the same test
|
||||
message. Added thumbs-up at **18:33:54.321 UTC**; delivery
|
||||
`88ab7e27-6ef9-4cb8-9f9b-400a81842026` was received at **18:33:55.201** and
|
||||
processed at **18:33:55.204**. Without navigating or reloading that Activity
|
||||
view, the new row and callback-health timestamp were visible at **18:34:07.073**.
|
||||
Removed the reaction at **18:34:07.139**; delivery
|
||||
`39b4632e-ee2a-4874-94d8-b5fa7d33d643` was received at **18:34:07.888** and
|
||||
processed at **18:34:07.890**. The removal was also visible without
|
||||
navigation at **18:34:31.803**, and both rows were visually inspected after
|
||||
scrolling. These observation times establish automatic updates, not a measured
|
||||
five-second end-to-end latency guarantee or a comprehensive transition audit.
|
||||
|
||||
## Repeated Discord cycle defect
|
||||
|
||||
Repeating the same Discord thumbs-up at **18:31:01.614** and removing it at
|
||||
**18:31:19.087** produced no additional delivery rows. A later add at
|
||||
**18:32:59.051** also produced none. This is distinct from the UI cache defect:
|
||||
the database itself still held only the original add/remove pair. All added
|
||||
test reactions were subsequently removed.
|
||||
|
||||
The provider event ID hashes the raw reaction payload. Discord's repeated
|
||||
payloads had the same fingerprint, so event-kind plus payload distinguished
|
||||
addition from removal but not a later occurrence of either. Qualification of
|
||||
repeated Discord reaction cycles is failed at this checkpoint. The follow-up
|
||||
must retain stable provider dispatch identity so actual duplicate delivery is
|
||||
still deduplicated while distinct add/remove cycles remain auditable. No
|
||||
history row was fabricated or replayed to claim a pass.
|
||||
|
||||
The follow-up adapter revision `paperclip-discord-v5` preserves the Gateway
|
||||
session fingerprint, shard, event type, and sequence with the exact raw packet.
|
||||
Only a one-way session fingerprint is carried, never the resumable session ID.
|
||||
A packet-scoped WeakMap and a guarded, synchronous packet-handler wrapper keep
|
||||
identity intact when discord.js buffers startup events. Suppressed SDK events
|
||||
cannot leave a stale identity for the next event. Resumed duplicate dispatches
|
||||
keep their identity; a new READY session receives a different fingerprint.
|
||||
The wrapper is restored during shutdown and missing pinned hooks fail closed.
|
||||
|
||||
After synchronizing the installed package with the tracked patch, its complete
|
||||
reverse dry-run passed and focused adapter/runtime tests passed **110/110**,
|
||||
including buffered processing, suppressed callbacks, replay, session replacement,
|
||||
and hook restoration. An initial reverse check differed only in the formatting
|
||||
of the existing `ensureRootThread` helper; no semantic change to that helper was
|
||||
needed. Live repeated-cycle qualification still requires the restart below.
|
||||
|
||||
## Telegram prior-generation reaction defect
|
||||
|
||||
In the same signed-in browser, added thumbs-up to the native file-reading reply
|
||||
at **18:39:26.594 UTC** and removed it at **18:40:13.685**. Telegram showed the
|
||||
reaction, but Paperclip recorded neither event. That provider message
|
||||
`417200359:43` belongs to completed DM generation **5**, conversation
|
||||
`3f13f43b-b9a7-44d9-9b8c-846ce77b0305`.
|
||||
|
||||
As a control, reacted to the newer native image-reading reply at
|
||||
**18:40:25.370**. Its message `417200359:45` belongs to active generation **6**,
|
||||
conversation `2b080821-103d-478e-b421-462014db7b30`. Delivery
|
||||
`ef99baa0-19f9-43e2-bd17-ec3fc3426a5f` was received at **18:40:25.557** and
|
||||
processed at **18:40:25.559**, with no error. Removed this test reaction at
|
||||
**18:40:49.340**; delivery `31f43dcc-2d99-4f13-b6ba-f9dbced3dcf5` was received
|
||||
at **18:40:49.533** and processed at **18:40:49.535**. No message was sent and
|
||||
no bot acknowledgement was changed.
|
||||
|
||||
The reaction handler chose the newest conversation before checking the exact
|
||||
message link, so an older generation's message was silently dropped. The new
|
||||
regression failed before the fix with zero rows instead of four. The fix
|
||||
resolves the conversation through its exact, company/endpoint/thread-scoped
|
||||
message link before choosing a generation. Existing current reach, principal
|
||||
authorization, runtime fencing, and conversation-state checks still apply.
|
||||
The first focused PostgreSQL reaction run passed **9/9**; this is local
|
||||
supporting evidence, pending live retest on the restarted backend.
|
||||
|
||||
## Follow-up integration checkpoint
|
||||
|
||||
The full PostgreSQL chat integration suite passed **266/266** on fresh migrated
|
||||
database `chat_adapters_test_20260907_latency_13`, including distinct Discord
|
||||
cycles, exact replay suppression, new-session identity, old Telegram DM
|
||||
generation ownership, wrong-thread rejection, and revoked DM reach. Server
|
||||
TypeScript and `git diff --check` passed. The preceding full run passed 265
|
||||
cases and failed only because the new test used an unsupported `toHaveSize`
|
||||
assertion; it was corrected to inspect the Set's size before this clean run.
|
||||
|
||||
The lockfile was not changed. The only newly fetched upstream commit,
|
||||
`392ab26b1`, changes that file alone and was not applied under the explicit
|
||||
instruction to leave it untouched.
|
||||
|
||||
## Final live retest on the reaction fix
|
||||
|
||||
Commit `dde176bbc` was pushed and the isolated server restarted after verifying
|
||||
zero active runs. Snapshot 10 started at **19:18:20.078 UTC**, with loaded server
|
||||
version `2026.831.0+396.git.dde176bbc`; startup recovery reached ready and the
|
||||
Discord Gateway connected. Existing recovery-blocked history was not edited.
|
||||
|
||||
The same signed-in browser repeated two thumbs-up add/remove cycles on each
|
||||
original test message. Every event below was processed once with a null error.
|
||||
|
||||
| Provider | Event | Browser action UTC | Received → processed UTC | Delivery ID |
|
||||
| -------- | -------- | ------------------ | --------------------------- | -------------------------------------- |
|
||||
| Discord | Add 1 | 19:18:51.109 | 19:18:51.537 → 19:18:51.539 | `324d147a-6ace-44fe-9952-529a02bfaced` |
|
||||
| Discord | Remove 1 | 19:18:56.075 | 19:18:56.336 → 19:18:56.338 | `d98047bd-9efb-48b4-98b5-ed79f2aea964` |
|
||||
| Discord | Add 2 | 19:19:10.424 | 19:19:10.661 → 19:19:10.663 | `71185c1a-bf33-4b75-87d9-9ba530c417e1` |
|
||||
| Discord | Remove 2 | 19:19:15.631 | 19:19:15.826 → 19:19:15.829 | `8e45ba6c-94f7-40cb-b4f3-d62a681fcfc7` |
|
||||
| Telegram | Add 1 | 19:19:31.957 | 19:19:33.065 → 19:19:33.067 | `5e1a4207-cbb0-49ff-87c7-c400f2cde12c` |
|
||||
| Telegram | Remove 1 | 19:19:52.642 | 19:19:52.854 → 19:19:52.856 | `e3494e32-9841-4f6e-894b-d1d65ac26eb0` |
|
||||
| Telegram | Add 2 | 19:20:15.526 | 19:20:15.711 → 19:20:15.714 | `e41b4e64-750d-4ae0-a59d-66f37edba765` |
|
||||
| Telegram | Remove 2 | 19:20:33.120 | 19:20:33.386 → 19:20:33.387 | `267118ca-5d62-4c57-8856-eeaf55f6400b` |
|
||||
|
||||
Discord's second pair appeared in its already-open Activity view by
|
||||
**19:19:21.917**, without reload/navigation. Telegram's first pair appeared
|
||||
after navigating from the catalog into Activity; no automatic-refresh claim is
|
||||
made for that first pair. With that view kept open, its second addition was
|
||||
visible at **19:20:28.587** and removal at **19:20:40.938**. Both providers'
|
||||
latest rows were visually inspected. All four Telegram receipts belong to
|
||||
completed generation 5, not the newer active generation 6.
|
||||
|
||||
Between **19:18:41.974** and **19:20:57.941 UTC**, counts stayed **86 Maya runs,
|
||||
17 tasks, 216 comments, 200 publications**. All test thumbs-up reactions were
|
||||
removed; existing bot reactions stayed intact. Slack, GitHub, Discord, and
|
||||
Telegram endpoints remained active. These specific functional and Activity
|
||||
freshness retests pass and supersede the failed reaction baselines above.
|
||||
Replay/startup-buffer behavior has deterministic coverage, not an injected
|
||||
live Gateway outage qualification.
|
||||
|
||||
The Maya agent was rechecked as `paperclip_runner` → `codex` → `gpt-5.6-luna`.
|
||||
The current Codex usage tool still reports the general weekly limit exhausted;
|
||||
no reset, billing change, or model-starting prompt was attempted in this batch.
|
||||
|
||||
The broader provider release gate remains open, including live native model
|
||||
stress/recovery and Teams tenant/admin qualification.
|
||||
|
|
@ -0,0 +1,388 @@
|
|||
# Native Paperclip runner chat qualification — 2026-09-07
|
||||
|
||||
## Scope and runtime
|
||||
|
||||
The live Maya E2E fixture was switched from legacy ACP/Sol to
|
||||
`adapterType: paperclip_runner`, Codex provider, model `gpt-5.6-luna`.
|
||||
The isolated instance has native execution enabled. Persisted run records
|
||||
confirm `runtimeMode: native`, `driverKind: codex_app_server`, and the explicit
|
||||
Luna model in the native execution input. This is not an inference from the
|
||||
agent's display name. No global model defaults were changed.
|
||||
|
||||
The native runner uses its per-turn lifecycle. The legacy agent configuration's
|
||||
reasoning-effort field is not propagated by the current native input contract;
|
||||
these measurements must not be described as native low-effort measurements.
|
||||
Terra has not been needed for the text cases below and has not been qualified.
|
||||
|
||||
Tests used the existing linked operator identities and existing private test
|
||||
threads. Guest isolation and permission requirements were not relaxed.
|
||||
|
||||
## Why the earlier turns were slow
|
||||
|
||||
Earlier legacy runs spent most of their time in extra model/tool round trips,
|
||||
not in the provider transport or queue. Examples of successful media turns took
|
||||
150–183 seconds while their actual tool execution totaled under two seconds.
|
||||
Generic operational instructions also asked agents to repeat checkout, comment,
|
||||
and status work already owned by the chat harness.
|
||||
|
||||
The new narrow external-chat response contract is enabled only after validating
|
||||
the company, immutable agent, active endpoint/conversation, exact issue, inbound
|
||||
comment lineage, known provider, and harness checkout. Governed, held, recovery,
|
||||
truncated, and otherwise ambiguous contexts retain the normal workflow.
|
||||
Self-contained replies need no redundant control-plane calls. Files and real
|
||||
work still require the authorized tools and normal safety checks.
|
||||
|
||||
## Live native text results
|
||||
|
||||
After restarting with the latest-message fix, each provider received
|
||||
“What is 61 + 8? Reply with only the number.” Telegram's wording additionally
|
||||
made explicit that this was a new message. All four provider UIs showed `69`.
|
||||
|
||||
| Provider | Native run ID | Agent runtime | Send to publication acknowledgement |
|
||||
| -------- | -------------------------------------- | ------------: | ----------------------------------: |
|
||||
| Discord | `a37453a9-9a01-42bc-a6c2-73b1a1793761` | 11.169 s | 13.472 s |
|
||||
| Telegram | `b950314c-1a62-4046-8db7-2dd31f1fc27a` | 12.288 s | 14.743 s |
|
||||
| Slack | `9001a484-ce26-489f-99f9-26446d979447` | 11.015 s | 13.554 s |
|
||||
| GitHub | `7ba05f32-5cb4-4477-b3a5-1c07d537a8e9` | 11.493 s | 16.467 s |
|
||||
|
||||
Agent runtime is persisted `finishedAt - startedAt`. The final column is the
|
||||
browser send timestamp to Paperclip's provider publication acknowledgement,
|
||||
not a measured client-render latency. Run-row queue delays were 8–11 ms.
|
||||
These are small local qualification samples, not production percentiles or an
|
||||
SLA. Images, files, investigation, and externally delayed callbacks can take
|
||||
longer.
|
||||
|
||||
### Correctness failure found and fixed
|
||||
|
||||
The first native Telegram turn answered an old task-title instruction instead
|
||||
of the current arithmetic question. Its run was
|
||||
`63477f30-2f08-4644-9ec8-516ecbde2b89`. The wake comment was correct; the native
|
||||
structured title and completion objective repeated the old imperative.
|
||||
|
||||
Verified external-chat turns now use neutral structured native task fields.
|
||||
The canonical task title/description remain background context. Completion
|
||||
contracts target the latest message, and coalesced comments become ordered
|
||||
criteria. Resumed eligible chat turns use the safe compact context selector.
|
||||
The succeeding Telegram run above returned the correct current answer.
|
||||
|
||||
### Live burst/queue test
|
||||
|
||||
Three messages were submitted rapidly in each provider's existing thread:
|
||||
requests for `ALPHA-0907`, `BETA-0907`, and `GAMMA-0907`. In all four provider
|
||||
UIs, the first run answered ALPHA and the following run answered BETA and GAMMA
|
||||
together. The latter run's persisted wake IDs contained both pending messages.
|
||||
No requested marker was omitted and no duplicate final answer was observed.
|
||||
|
||||
| Provider | First run | Coalesced follow-up run |
|
||||
| -------- | -------------------------------------- | -------------------------------------- |
|
||||
| Discord | `948adf45-9264-4060-84fd-66dcdb2ffb5b` | `1e4b610e-a61e-4054-b3b3-adb1c2b6d241` |
|
||||
| Telegram | `61044c8b-a6ab-4f7a-afa5-41aa3239b00c` | `efb8fee1-15ad-4b83-bd62-aa6a8a7e1895` |
|
||||
| Slack | `838b739d-0361-4a01-bc3f-48703b65d426` | `bccb60a2-4f52-4153-b1e9-62354b8dbe27` |
|
||||
| GitHub | `abed454d-5e7c-45d1-a4b1-80b152beb160` | `8ddd5631-54f2-4c3b-b3d2-41dbd8002fa9` |
|
||||
|
||||
Pending messages wait for the current turn before their run is materialized;
|
||||
the run-row queue metric alone does not include this intentional wait.
|
||||
|
||||
A subsequent ten-message Discord test reached `requestedCount: 10` with eight
|
||||
inline comments. The first held turn was
|
||||
`d7f361e3-0631-4ffe-975f-a331b65016ec`; follow-up
|
||||
`fd08101d-66a7-4a79-b485-bfb3ff2816b7` could not access the scoped reader in its
|
||||
older resumed provider session and did not produce a complete answer. This is
|
||||
a failed qualification, not a ten-message success. Discord briefly throttled
|
||||
the browser's rapid sends; the remaining messages were sent normally and all
|
||||
ten were durably accepted before the follow-up run.
|
||||
|
||||
The final checkpoint fingerprint is versioned by both tool schema and
|
||||
advertisement policy, including the stable, binding-gated reader. It also
|
||||
distinguishes local versus remote tool sets, rejects different managed
|
||||
execution workspaces, and permits projectless continuation only when both
|
||||
run-local workspace placeholders and repository descriptors match. Native
|
||||
overflow turns use neutral task framing and explicitly opt into the reader;
|
||||
legacy adapters retain their existing authenticated API fallback.
|
||||
|
||||
The live retry on the new checkpoint contract was blocked by account capacity:
|
||||
Slack run `6299ba19-d7ea-4182-b7b8-c401d722821e` received an actual Codex
|
||||
`turn.failed` with `codexErrorInfo: usageLimitExceeded`. It was a 10,402-character
|
||||
message (the Slack composer would not send the initial 17,122-character draft),
|
||||
with four values spanning the truncated inline body. The final reader fix is
|
||||
therefore locally verified but **not live requalified**. A paced Discord retry
|
||||
also hit the same provider-capacity boundary; its ten-message test was not
|
||||
completed. Follow-ups on that recovery-owned task were rejected by the staging
|
||||
ownership check; those recovery-path messages need live follow-up after capacity
|
||||
returns, without loosening ownership checks.
|
||||
|
||||
The newly observed quota failure now has a closed classification from a
|
||||
committed provider terminal, stops futile automatic retries, and produces a
|
||||
safe provider-facing capacity explanation. Model prose, tool output, raw
|
||||
provider error strings, account details, and reset URLs are not used as public
|
||||
error content. This last change is locally tested; it has not been live tested.
|
||||
|
||||
### Post-live failure and attachment-isolation regressions
|
||||
|
||||
A database-backed restart test reproduced a further capacity-error bug: after
|
||||
the provider terminal was committed but the controller stopped before its
|
||||
callback, replay of that exact event lost the usage-limit classification and
|
||||
scheduled another attempt. The duplicate-event observer now restores only that
|
||||
in-memory classification. It does not repeat logging, activity, or publication.
|
||||
Both first delivery and exact replay now persist `terminal_failure`, no next
|
||||
attempt or automatic wake, a board-owned capacity recovery action, and exactly
|
||||
one durable provider terminal. The test seeds the post-commit crash boundary and
|
||||
uses a simulated provider with real PostgreSQL; it is not a process-kill test or
|
||||
a new live Codex call.
|
||||
|
||||
The task UI maps the closed native capacity code to “Usage limit reached” and
|
||||
explains when to retry, without exposing provider account details. The explicit
|
||||
retry callback remains subject to the existing server checks. The earlier live
|
||||
failure retains its original recorded error; it was not rewritten to fabricate
|
||||
post-fix UI evidence.
|
||||
|
||||
Two additional database-backed file tests place an older decoy attachment on
|
||||
the same task. With a newer current-wake attachment, only the newer storage
|
||||
object is read and staged; with an omission-only current wake, no storage object
|
||||
is read and no file is staged. This verifies isolation, not the ability to
|
||||
retrieve or resend a historical attachment on request. That separate user
|
||||
journey remains unqualified.
|
||||
|
||||
## Slack callback recovery
|
||||
|
||||
The exact `maya-e2e-paperclip` app, `A0C03GA5FPU`, still had a verified Events
|
||||
callback on the older `:10000` Funnel endpoint. One message arrived only after
|
||||
approximately six minutes of provider retries. Its callback was changed in the
|
||||
signed-in Slack configuration UI to canonical `:8443`, verified by a genuine
|
||||
Slack challenge, and saved. The similarly named older app was not changed.
|
||||
|
||||
The app's Interactivity and existing slash-command callbacks were also updated
|
||||
to the same canonical URL. A real `/maya-e2e-fjomcs status` invocation reached
|
||||
the new callback. A native single-choice question rendered Red/Blue buttons;
|
||||
clicking Blue reached the genuine interactive callback, updated the card to
|
||||
“Answered: Blue,” and produced the native continuation answer `Blue`.
|
||||
All three callback surfaces became `current`, and `callbacksNeedUpdate` became
|
||||
false.
|
||||
|
||||
The isolated webhook-only proxy now preserves its allowlisted public HTTPS
|
||||
origin, and the server trusts forwarding headers only from loopback. This also
|
||||
fixes false stale-callback observations caused by rewriting the host to local
|
||||
HTTP. Public Board requests and wrong-host requests returned 404; an unsigned
|
||||
request to a known webhook returned 401. The Board remains private.
|
||||
|
||||
## Runner activity and files
|
||||
|
||||
Native activity is durably recorded in Paperclip's run-event path and consumed
|
||||
by the task transcript UI. Focused tests cover native transcript projection,
|
||||
polling, and task rendering. External publication remains a separate safe
|
||||
projection: coarse lifecycle status and selected final answers. Raw reasoning,
|
||||
tool names/arguments/results, credentials, and internal logs are not chat output.
|
||||
|
||||
The live Paperclip task UI was inspected: native turns show worked duration,
|
||||
expandable tool activity, and the queued/delivered timestamps for burst inputs.
|
||||
The corresponding provider thread contains the selected answers, not the
|
||||
internal operational commentary.
|
||||
|
||||
Follow-up read-only audit on September 7 reconfirmed the live agent configuration
|
||||
as `paperclip_runner` / `codex` / `gpt-5.6-luna`, and the four text-run records
|
||||
above as `native` / `codex_app_server`. No global defaults were changed. Focused
|
||||
projection, stream, run-publication, interaction-publication, and heartbeat
|
||||
summary tests passed **82/82** across five files. No live model call was made for
|
||||
this follow-up because the account quota remains exhausted.
|
||||
|
||||
The Board's rich native activity projection is not safe to forward wholesale:
|
||||
its objects can include command output, targets, and research queries. Native
|
||||
`report_progress` is also Board-only for chat-origin runs. External stream
|
||||
chunking presents already-selected safe prose; it is not token-live Runner
|
||||
reasoning. Any richer external activity would require a separate closed,
|
||||
sanitized projection, not reuse of the Board transcript objects.
|
||||
|
||||
A separate native execution-input, question-bridge, file-handoff, and
|
||||
same-conversation attachment-reuse recheck passed **20/20** across four files
|
||||
after the Slack polling change. Its disabled-runner eligibility error was an
|
||||
isolated test-fixture gate, not a failure of the active live instance. These
|
||||
contract/DB checks do not substitute for new model-driven live turns.
|
||||
|
||||
The new runner does not have the legacy operational skill or a general
|
||||
Paperclip API key. Consequently, the previous shell-helper file instructions
|
||||
were not a valid native-runner qualification. Native runs now receive a scoped
|
||||
`register_deliverable` tool for local files and run-bound staging descriptors
|
||||
for incoming attachments. Registration means prepared, not delivered; the
|
||||
existing audited publication path still owns provider delivery.
|
||||
|
||||
### Native media evidence
|
||||
|
||||
| Provider / case | Native run | Observed result |
|
||||
| -------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Telegram, generated text file | `8047d0ca-aabf-42d7-b8a9-753a84edbade` | Actual `native-telegram-0907.txt` document, 23 bytes; exact content `NATIVE-TELEGRAM-0907-OK`, no newline. |
|
||||
| Telegram, inbound text | `21e85d82-f13e-4e9a-9003-7f8f08834d36` | Read new 103-byte fixture and returned the correct unseen phrase `violet birch 82`; 21.099 s. |
|
||||
| Telegram, image round-trip | `c7fa20f1-d2d5-4ec2-91b2-00293735e5d5` | Described the orange tabby, sofa, and plant, then returned an actual photo; visually opened and inspected. 38.885 s. |
|
||||
| Discord, generated text file | `d97f9736-f4be-417b-b38e-cf452f22f245` | Actual `native-discord-0907-c.txt` attachment and inline content preview; exact 24 bytes, no newline. 45.663 s. |
|
||||
| Slack, generated text file | `71e4a887-21b1-4ecb-8b00-8a875aef04d6` | Actual `native-slack-0907-b.txt` file with preview in the original thread; exact 22 bytes, no newline. 38.576 s. |
|
||||
| GitHub, generated file fallback | `cf8c2192-4643-4983-905e-2258c0a4162b` | Canonical `native-github-0907-b.txt`, exact 23 bytes, no newline. GitHub explicitly reported private-task storage and that this App cannot upload file bytes into comments. 43.761 s. |
|
||||
| Discord, combined incoming text/image retest | `7f6cd1db-321b-4a02-a764-8db7496b1e19` | Exact phrase `violet birch 82`, correct cat/green-eyes/plant description, and actual returned PNG; 54.778 s. |
|
||||
| Slack, combined incoming text/image retest | `9bcc22ea-14dd-4e57-ac12-c22dad7b2f95` | Exact phrase and correct cat/sofa/plant description in the final answer, with returned image visibly rendered in the original thread; 55.855 s. |
|
||||
|
||||
The returned Telegram JPEG matched the received image's 221,327 bytes and
|
||||
SHA-256 `1d22f8c026abf16ff0dde087d6c46a3b4a41978cfb4cee62c62e159e5550ce8a`.
|
||||
The inbound attachment was `b00400f3-7166-4e9c-af49-6a6091d783cd`; the
|
||||
run-originated outbound attachment was `c238a817-fd79-4608-8db3-efd958efe1ee`.
|
||||
Telegram may compress a newly uploaded photo, so this compares the received
|
||||
provider image with the returned file, not with the original local PNG.
|
||||
|
||||
### Failures discovered during native qualification
|
||||
|
||||
- Native direct mode originally stripped all dynamic tools. The first Discord
|
||||
file run `29ab85fc-e13a-4bbf-aa99-5d31094b0ba7` could create a file but could
|
||||
not register it. The direct-mode bridge now permits only the server-supplied
|
||||
file handoff capability, not arbitrary semantic/governance tools.
|
||||
- Existing resumed Codex threads still lacked that newly added tool even when
|
||||
it was passed to `thread/resume`. Discord run `567d3ae4-7627-4805-a925-b84c194eb58b`,
|
||||
Slack run `ec6e140b-828c-4d77-ad6d-e441bb4e0ff4`, and GitHub run
|
||||
`bba6a25d-c417-4360-a6ef-e161b5858a68` exposed this. A persisted tool-contract
|
||||
fingerprint now rejects incompatible checkpoints before compact prompting;
|
||||
fresh provider sessions receive the complete context and tool set. The
|
||||
successful Discord/Slack/GitHub retries in the table above used this fix.
|
||||
- A native status-decision wake could overwrite `chat:discord` provenance when
|
||||
coalescing, preventing terminal publication and leaving “working” visible.
|
||||
Exact status-decision metadata is now separate and preserves the original
|
||||
verified chat source; unmarked/unrelated sources do not get that treatment.
|
||||
- Corrupt or incompatible checkpoints previously rotated the session ID but
|
||||
retained a resume-only prompt. The constructor now rebuilds full task and
|
||||
wake context. File-only comments also retain a current completion criterion
|
||||
instead of falling back to an older task title.
|
||||
- Discord's first combined incoming text/image test, run
|
||||
`07a0237a-f271-4add-9889-591a3cf0a515`, exposed a MIME parsing bug:
|
||||
`text/plain; charset=utf-8` was rejected by the attachment allowlist. The
|
||||
image arrived, but Luna substituted an older generated text file. This is
|
||||
a failed content-correctness test, despite successful image delivery.
|
||||
- Slack's equivalent run `812c7ea0-5341-4d55-9290-6c3d0bfe2f09` read both
|
||||
attachments correctly in private activity, then omitted the requested
|
||||
phrase and description from its semantic completion summary. The final
|
||||
answer contract now explicitly requires the requested answers in that
|
||||
summary; publishing private commentary is not the fix. Both multimodal
|
||||
cases passed the unchanged live request after the MIME and current-attachment
|
||||
guidance changes, as recorded above. Terra was not needed. Each returned
|
||||
image exactly matched its provider's incoming bytes and SHA-256: Discord
|
||||
2,111,878 bytes, `7693966f6c2b4aaebf9e46359f715fdaede021346bcd926078bb331b1dddc3c1`;
|
||||
Slack 2,088,249 bytes, `005f8dabdb19ef786c0e2e76695596d22c1d0bb53de374e0be209cc6d89851c9`.
|
||||
|
||||
GitHub browser qualification briefly encountered a different active signed-in
|
||||
account (`forgottendev`). The existing account switcher restored `cryppadotta`;
|
||||
the stale page's optimistic comment was not treated as a successful delivery.
|
||||
|
||||
After the native Telegram file and image turns, the server-created local
|
||||
staging slot was verified to contain zero bytes. Cleanup retains exact file
|
||||
descriptors rather than deleting mutable paths. The final cross-process design
|
||||
also skips live/unknown foreign owners and handles PID reuse. Opaque zeroed
|
||||
directories remain per server restart; a process reuses its own slots, bounded
|
||||
by its peak concurrent staged attachment count, not by sequential turns.
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
- Full chat integration suite on fresh PostgreSQL database
|
||||
`chat_adapters_test_20260907_latency_03`: **263/263**.
|
||||
- Repeated full chat integration on fresh
|
||||
`chat_adapters_test_20260907_latency_04`: **263/263**.
|
||||
- Final full chat integration on fresh
|
||||
`chat_adapters_test_20260907_latency_06`: **264/264**, including Discord MIME
|
||||
parameters and durable attachment-omission notices. The intervening `_05`
|
||||
run exposed two fixture scheduling races; the cold-start and restart tests
|
||||
now synchronize/seed their intended crash boundary without weakening their
|
||||
acknowledgement-budget or revoked-access assertions.
|
||||
- Repeated final full chat integration on fresh
|
||||
`chat_adapters_test_20260907_latency_07`: **264/264** after the final native
|
||||
compatibility, framing, and capacity-error changes.
|
||||
- Adapter utility and ACP execution tests: **261/261**.
|
||||
- Focused server/native/UI transcript tests: **206/206**, before the subsequent
|
||||
native file-handoff changes.
|
||||
- Capability inventories/contracts regenerated from the changed operational
|
||||
skill; drift checks pass and validator self-tests pass **4/4**.
|
||||
- Deterministic connector browser suite: **5/5** (provider APIs are mocked in
|
||||
this suite; the live evidence above is separate), repeated after the native
|
||||
file and reader changes.
|
||||
- Native latest-turn, checkpoint fallback, and heartbeat context tests:
|
||||
**51/51** after the file-only/current-context fixes.
|
||||
- Native handoff/executor tests: **145/145**; Codex driver tests: **65/65**.
|
||||
- Server TypeScript and runner build (including Rust binary and generated
|
||||
contract/replay checks) passed, repeated after the generated skill contracts.
|
||||
- Final reader/storage/authority tests: **20/20**, including five reader DB
|
||||
cases and a failed receipt write that rolls back the exact storage object.
|
||||
- Final executor/checkpoint/reader/native framing tests: **168/168**.
|
||||
- Inline/overflow and legacy-adapter contract tests: **284/284**, plus the
|
||||
independent native overflow-framing test **1/1**.
|
||||
- Final Codex driver suite: **65/65**. Shared, UI, adapter-utils, and server
|
||||
TypeScript checks passed. The operational skill validator passed.
|
||||
- Final combined native runtime, reader, file-handoff, framing, capacity-error,
|
||||
adapter, heartbeat context, attachment-type, and operational skill regression
|
||||
run: **552/552**. Capability inventory and generated-contract drift checks
|
||||
passed again. These are focused tests, not a claim that the workspace-wide
|
||||
test/build gate passed.
|
||||
- Post-live capacity replay, attachment-isolation, control-plane port, native
|
||||
executor/reader, safe publication, and task UI checks: **256/256** across
|
||||
seven focused suites. The replay test failed before the observer fix by
|
||||
scheduling a new attempt, then passed. Server/UI TypeScript and UI token
|
||||
gates passed. These simulated capacity cases do not replace the blocked
|
||||
live quota-recovery retest.
|
||||
|
||||
Broad workspace tests are not claimed green. Teams still needs the real Microsoft 365
|
||||
tenant/admin setup and has not received equivalent native live qualification.
|
||||
GitHub App file-byte uploads remain an explicitly disclosed private-task
|
||||
fallback. Provider quota recovery, the final long/burst reader changes, and
|
||||
historical attachments outside the current wake still require live qualification;
|
||||
this document is not a production-readiness sign-off for all providers/features.
|
||||
|
||||
## Same-conversation file resend follow-up
|
||||
|
||||
Code review after the native live file tests found a real capability gap:
|
||||
current-wake staging safely omitted older files, but native direct chat had no
|
||||
bounded way to resend an earlier attachment. The follow-up adds
|
||||
`list_chat_attachments` (paged metadata only) and `reuse_chat_attachment`
|
||||
(exact-byte server-side copy into a new current-run attachment and final-response
|
||||
selection). It does not expose storage locations, reopen general task tools, or
|
||||
permit an older file to substitute for unavailable current-turn input.
|
||||
|
||||
Both operations verify the current native run, immutable endpoint agent,
|
||||
conversation, destination reach, principal membership, and exact admitted
|
||||
inbound or confirmed published file lineage. Reuse repeats authorization on
|
||||
idempotent replay, rejects ask-mode mutation, and records source/new IDs and
|
||||
SHA-256 in receipts and Activity. Deleted or provider-edited/deleted source
|
||||
messages are ineligible. The byte handoff also works for remote native targets
|
||||
without returning a local path. The native tool-contract fingerprint advances
|
||||
to v3 so old provider sessions cannot silently retain the pre-resend tool set.
|
||||
|
||||
Supporting verification after review fixes:
|
||||
|
||||
- Reuse/authority/resume suites: **35/35**; the three DB cases cover byte
|
||||
identity, duplicate suppression, receipt preservation, equal-timestamp
|
||||
pagination, exact-pair lineage, and access/source revocation.
|
||||
- Codex driver fresh/resumed direct-mode tool filtering: **65/65**.
|
||||
- Executor, file handoff, current-wake reader, and capacity regression:
|
||||
**156/156**.
|
||||
- Native chat prompt context tests: **29/29**.
|
||||
- Server TypeScript and full runner build passed, including generated
|
||||
protocol/capability/semantic drift checks, workflow traceability, Rust binary,
|
||||
and replay golden checks. The lockfile was unchanged.
|
||||
|
||||
These are local simulated/DB tests. New live model-driven historical-file resend,
|
||||
long/burst reader, and quota-recovery qualification remain blocked by the actual
|
||||
Codex `usageLimitExceeded` response. Historical-file resend is not historical
|
||||
file inspection: this tool intentionally returns no earlier file bytes to the
|
||||
model. Teams and GitHub's private-task attachment fallback retain the limits
|
||||
described above.
|
||||
|
||||
### Historical-file discovery and bounded storage follow-up
|
||||
|
||||
Commit `3e932de55` fixes a narrower discovery defect: selecting the newest
|
||||
publication before excluding deleted or edited provider messages could hide an
|
||||
older, still-valid publication of the same attachment. Listing now filters
|
||||
invalid lineages before choosing a candidate. An explicit request using the
|
||||
older known source-comment pair could already succeed; the defect was not a
|
||||
blanket inability to reuse that file.
|
||||
|
||||
The same follow-up requires a provider message ID and publication timestamp for
|
||||
confirmed outbound lineage, scopes inbound joins to the exact endpoint and
|
||||
conversation, validates cursor UUIDs before querying, and bounds storage reads,
|
||||
writes, and cleanup. A write that completes after its timeout schedules cleanup
|
||||
of that exact newly written object.
|
||||
|
||||
The expanded package-local database suite passed **5/5**, covering valid older
|
||||
lineage, unconfirmed publication rejection, malformed cursors, stalled writes
|
||||
and late cleanup, byte identity, idempotency, and source/access revocation.
|
||||
Server TypeScript passed. This is supporting local verification, not a new
|
||||
live model-driven resend or quota-recovery pass; those remain unqualified.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,211 @@
|
|||
# GitHub private attachment authority
|
||||
|
||||
This records the bounded implementation and qualification boundary, not a claim
|
||||
that arbitrary private GitHub files are downloadable by an installation App.
|
||||
|
||||
## Supported authority
|
||||
|
||||
GitHub documents installation access tokens for the exact
|
||||
[issue-comment GET](https://docs.github.com/en/rest/issues/comments#get-an-issue-comment)
|
||||
with existing Issues or Pull requests read permission. Its
|
||||
`application/vnd.github.full+json` representation includes both the original
|
||||
body and rendered HTML. The corresponding
|
||||
[review-comment GET](https://docs.github.com/en/rest/pulls/comments#get-a-review-comment-for-a-pull-request)
|
||||
requires Pull requests read and uses
|
||||
`application/vnd.github-commitcomment.full+json`.
|
||||
|
||||
Those are legitimate fixed-repository comment reads. They do not document a
|
||||
general private-attachment download API. GitHub's
|
||||
[attachment documentation](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/attaching-files)
|
||||
distinguishes anonymous public uploads from repository-gated private uploads;
|
||||
its [private attachment change](https://github.blog/changelog/2023-05-08-more-secure-private-attachments/)
|
||||
explains why knowing the original private URL is insufficient.
|
||||
|
||||
## Narrow implementation
|
||||
|
||||
1. Only attachment references from an admitted, exact provider comment receive
|
||||
a version-2 locator with the original body SHA-256. Existing four-field
|
||||
locators remain anonymous-only; replay does not invent missing provenance.
|
||||
2. An anonymous 401/403/404 may trigger one exact-comment GET using the existing
|
||||
installation App, fixed `api.github.com`, no query, and no redirects. PAT,
|
||||
cookie, user-token, unbound-installation, and custom-host fallbacks are absent.
|
||||
3. The authenticated response must match comment ID, repository, issue/PR,
|
||||
review-root when applicable, and the original body hash. Exactly one anchor
|
||||
must contain one image targeting the same asset UUID on
|
||||
`private-user-images.githubusercontent.com`, with a sole JWT query absent
|
||||
from the original body. Its link must be either the original asset URL or
|
||||
exactly its image URL. Both forms share one candidate count; duplicate,
|
||||
mixed, or conflicting same-asset renderings fail closed. HTML parsing is
|
||||
inert and bounded.
|
||||
4. The signed image target is ephemeral. Download requests never receive App
|
||||
credentials or cookies. Existing HTTPS/public-address pinning, redirect
|
||||
allowlisting, byte/MIME validation, 20-second file and 60-second download-batch
|
||||
budgets remain. The batch budget is not a hard total admission deadline.
|
||||
5. Current principal, runtime credential generation, destination, conversation,
|
||||
issue, and original input authority are checked before network access, after
|
||||
download, and under locks with attachment registration. Storage I/O is outside
|
||||
governance transactions; explicit revocation after storage removes the new,
|
||||
unregistered blob. No provider response HTML or signed query enters the
|
||||
delivery ledger, model context, or logs.
|
||||
|
||||
## Remaining gap and truthful UX
|
||||
|
||||
The real private text fixture under `/user-attachments/files/31948982/` remained
|
||||
unavailable to anonymous intake; its provider browser anchor remained the
|
||||
original file URL. The image-specific canonical mapping above does not invent
|
||||
a signed generic-file endpoint. Such files remain a current-input
|
||||
`download_unavailable` omission: no imported bytes, no claim of inspection, and
|
||||
no substitution of an older task file. Activity currently shows that closed
|
||||
omission rather than asserting that every 404 specifically means “private.”
|
||||
|
||||
Outbound is separate: the official
|
||||
[GitHub CLI uploader](https://github.com/cli/cli/blob/trunk/internal/attachments/client.go)
|
||||
allows OAuth, personal-access, and fine-grained personal-access tokens, not App
|
||||
installation tokens. Paperclip keeps the private-task output-file fallback and
|
||||
does not acquire extra repository permissions or impersonate the browser user.
|
||||
|
||||
## Qualification gate
|
||||
|
||||
Contract tests prove the real SDK's installation-token exchange and fixed
|
||||
guarded comment GET, exact body/source binding, old-locator compatibility,
|
||||
restart reconstruction, credential-free bytes, malformed/ambiguous rendering
|
||||
denial, and revocation during download/storage. They are not live provider proof.
|
||||
|
||||
On the signed-in provider browser, upload a new private image to the existing
|
||||
authorized test issue/PR comment, ask Maya to inspect that exact image, and
|
||||
compare the stored hash/bytes with the fixture. Verify that no signed URL or
|
||||
HTML is persisted. Repeat with a private text file; unless GitHub actually
|
||||
provides a separately reviewed supported representation, it must still report
|
||||
unavailable without substituting another attachment. Then test a review-comment
|
||||
image and a changed/deleted source. Do not make the repository public to obtain
|
||||
a passing result.
|
||||
|
||||
### Closed diagnostics for provider qualification
|
||||
|
||||
A first live private review-comment image was not imported; a signed anchor in
|
||||
the browser is not evidence of the App REST response. The product now emits
|
||||
only a closed `attachmentDiagnosticCode` beside the endpoint, issue, and
|
||||
delivery IDs in the existing rejection log. Codes distinguish App authority or
|
||||
request failure, exact source/body mismatch, missing rendering, unsupported
|
||||
generic files, ambiguous/denied mapping, and a valid same-UUID signed-image
|
||||
shape without the required original source anchor. In particular,
|
||||
`github_attachment_canonical_signed_anchor_only` detected and denied an exact
|
||||
signed anchor/image pair in the diagnostic-only deployment.
|
||||
|
||||
No response HTML, URL, JWT query, token, or provider error details enter these
|
||||
diagnostics. SDK-wrapped errors retain only exact whitelisted codes with bounded
|
||||
cause traversal; unknown errors collapse to a closed request-failed code.
|
||||
Durable current-input omissions and agent prompts still use only
|
||||
`download_unavailable`.
|
||||
|
||||
At 09:57:09 UTC on September 8, the actual App path emitted that signed-anchor
|
||||
diagnostic for the newly admitted private review-comment image. This proved the
|
||||
shape behind the unchanged source/body/repository/review-thread fences. The
|
||||
bounded follow-up accepts exactly one such pair, with identical link/image
|
||||
URLs and the same private-host/path/UUID/JWT checks. No additional host or
|
||||
credential authority was added. Contract and PostgreSQL restart tests cover
|
||||
both accepted forms, credential-free bytes, mixed/duplicate rejection, and
|
||||
unchanged current-access/revocation checks. Successful live byte intake and
|
||||
agent inspection were still unqualified at that diagnostic checkpoint.
|
||||
|
||||
### Live main-conversation image and generic-file check
|
||||
|
||||
At **17:12:44.815 UTC**, root used the signed-in GitHub browser to upload a
|
||||
new image into the existing authorized test PR's main conversation. The
|
||||
repository was visually confirmed **Private** and its visibility and App
|
||||
permissions were not changed. `PRIVATE-IMAGE-LANDING-0908` asked the agent to
|
||||
inspect only that newly attached image, without suggesting its visual content.
|
||||
|
||||
The current-input attachment imported as PNG, **2,111,878 bytes**, SHA-256
|
||||
`7693966f6c2b4aaebf9e46359f715fdaede021346bcd926078bb331b1dddc3c1`.
|
||||
An independent read-only audit rehashed the stored blob and verified the exact
|
||||
comment/body/asset locator and wake-comment binding. The source-body hash
|
||||
matches GitHub's CRLF normalization, not a rendered-page reconstruction.
|
||||
The native `gpt-5.6-luna` run took **20.846 seconds** and recorded two artifact
|
||||
view events. The final response accurately described the orange tabby, pale
|
||||
green eyes, and indoor background; root inspected the rendered reply and image.
|
||||
The final arrived **26.303 seconds** after submission. Working and final
|
||||
operations each used one attempt and updated the same provider comment.
|
||||
|
||||
At **17:14:02.540 UTC**, `PRIVATE-FILE-LANDING-0908` uploaded a fresh synthetic
|
||||
private text file. The prompt did not reveal its shape, color, or count.
|
||||
The current-input action recorded exactly one `download_unavailable` omission,
|
||||
zero imported or generated attachments, and no image-view event. Luna
|
||||
truthfully reported that this exact new file was unavailable and did not
|
||||
invent or reuse values. Its final arrived in **21.116 seconds**.
|
||||
|
||||
Scoped delivery, action, wake, run, event, result, comment and publication
|
||||
checks found no persisted signed URL/JWT query or provider-rendered HTML.
|
||||
Original query-free author `<img>` syntax is not provider-rendered HTML.
|
||||
This is a scoped persistence audit, not a whole-database or browser-log claim.
|
||||
|
||||
These checks used server 48 and qualify the private **main-conversation**
|
||||
image path and honest generic-file omission. Generic private files and native
|
||||
outbound uploads remain unsupported.
|
||||
|
||||
### Live review-comment image after restart
|
||||
|
||||
On server 49, root replied to the existing authorized inline review thread,
|
||||
uploaded the fixture again as a **new provider asset**, and submitted
|
||||
`PRIVATE-REVIEW-IMAGE-LANDING-0908` at **17:25:14.894 UTC**. The final reply
|
||||
arrived in that same review thread at **17:25:45.922 UTC**, **31.028 seconds**
|
||||
later. The native Luna run used **23.822 seconds** and correctly described
|
||||
the cat, pale green eyes, pink chair and plant. The rendered response persisted
|
||||
after a normal browser refresh; the task and conversation remained open.
|
||||
|
||||
Independent diagnostics verified the exact new review comment, original review
|
||||
root, current Paperclip comment, source-body digest, and new asset UUID. The
|
||||
stored attachment again rehashed to the **2,111,878-byte** fixture SHA above.
|
||||
The supplied source matches the locator digest after GitHub CRLF normalization.
|
||||
There was one wake-associated run, no omission, two artifact-view events, and
|
||||
one attempt per working/final publication, both targeting the same provider
|
||||
review comment. Scoped persistence checks again found no signed-target/JWT or
|
||||
provider-rendered response HTML, and no internal identifier in the final text.
|
||||
|
||||
This extends live proof to new private images in **both** main conversations
|
||||
and review threads on the deployed authority implementation. It is not a
|
||||
changed/deleted-source test or an interrupted-download/revocation stress test.
|
||||
|
||||
### Live changed-source rejection
|
||||
|
||||
Root uploaded a fresh private image in the authorized PR conversation and
|
||||
submitted `PRIVATE-SOURCE-CHANGE-0908` at **17:38:59.379 UTC**. Paperclip was
|
||||
deliberately offline after a zero-active-run shutdown, so the original created
|
||||
delivery failed without entering the local delivery ledger. Root edited only
|
||||
that synthetic source comment at **17:39:33.981 UTC**, preserving its new image
|
||||
URL and appending a revision marker. The edit delivery also failed while the
|
||||
server was offline. Neither delivery had been admitted before restart.
|
||||
|
||||
After server 50 became ready, root used the existing App identity and GitHub's
|
||||
supported [App webhook redelivery API](https://docs.github.com/en/rest/apps/webhooks#redeliver-a-delivery-for-an-app-webhook)
|
||||
to redeliver **only the original created event**, once. The new signed
|
||||
delivery reached Paperclip and retained the original source digest
|
||||
`1ac24a2833edef198dd4d6dfa6155414f93dff5d6e01902f9ef65b6e7902244b`.
|
||||
The current canonical comment instead hashed to
|
||||
`6c47240d26bf98e6561479a9c01ac6c5e111766a46ba01182397aea4845c5514`.
|
||||
The unchanged new asset did not override this mismatch.
|
||||
|
||||
The closed diagnostic was `github_attachment_canonical_body_mismatch`, before
|
||||
signed-target selection. The original ingress action was processed and its
|
||||
retained body was redacted. No edited ingress action existed. The exact
|
||||
current input had one `download_unavailable` omission, zero imported or
|
||||
generated files, and no artifact-view event. One native Luna run took
|
||||
**14.881 seconds**. Its final publication used one attempt and arrived
|
||||
**17.755 seconds after ingress**: “The exact new image is unavailable because
|
||||
it could not be imported.” Root verified that visible answer in the provider
|
||||
conversation; the task remained open. The deliberate outage is not counted as
|
||||
ordinary response latency.
|
||||
|
||||
An independent scoped audit of 52 run events and the associated delivery,
|
||||
action, source, wake, run, result and publication found no signed-target/JWT
|
||||
or provider-rendered-HTML markers, and no internal UUID in the final output.
|
||||
This proves rejection of a **changed body** for a real redelivered event. It
|
||||
does not prove deleted-source or in-flight download revocation behavior.
|
||||
|
||||
Separately, the bot-created reply callback received a 502 before reaching the
|
||||
instrumented local proxy or Paperclip. GitHub reported the exact configured
|
||||
destination, a 0.1-second duration, no response headers and an empty body.
|
||||
Adjacent original-redelivery and bot-edit callbacks used that same destination
|
||||
and received 202. The bot-edit was correctly filtered as outbound/self; the
|
||||
missing created callback was not. The pre-proxy transport cause is unconfirmed
|
||||
and must not be described as harmless self-event filtering or a repaired bug.
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,132 @@
|
|||
# Native runner final-output burst benchmark — 2026-09-08
|
||||
|
||||
## Result and scope
|
||||
|
||||
A credential-free provider fixture emits 16, 128, or 512 ordered output deltas
|
||||
and completes immediately after one accepted semantic completion. The real
|
||||
Rust runner and TypeScript controller persist, acknowledge, replay, and close
|
||||
that turn. There is no model, provider network call, App database, or App
|
||||
semantic-completion grace period in this benchmark.
|
||||
|
||||
Batching only the provider queue acknowledgement for an already-durable prefix
|
||||
of at most 128 events reduced median provider-completion-to-visible-terminal
|
||||
time by 18–26%. Every individual PRP outbox save remains in place. Controller
|
||||
event commits, wire acknowledgements, authority checks, and exact suspension
|
||||
proof are unchanged. This is isolated benchmark evidence, **not a live chat
|
||||
latency or model-quality qualification**.
|
||||
|
||||
## Measurements
|
||||
|
||||
All values are milliseconds, shown as median [minimum–maximum], with **n = 3
|
||||
per size per binary**. Baseline repetitions ran first, then candidate repetitions
|
||||
on the same Mac. CPU scheduling, background work, filesystem caches, and I/O
|
||||
load were not controlled. These ranges are observations, not confidence bounds
|
||||
or p95 estimates.
|
||||
|
||||
| Deltas | Baseline: provider complete → visible terminal | Candidate: provider complete → visible terminal | Median reduction |
|
||||
| ------ | ---------------------------------------------- | ----------------------------------------------- | ---------------- |
|
||||
| 16 | 796 [787–815] | 653 [642–655] | 18.0% |
|
||||
| 128 | 5,486 [5,417–5,639] | 4,214 [4,125–4,222] | 23.2% |
|
||||
| 512 | 18,658 [18,633–19,102] | 13,745 [13,554–14,275] | 26.3% |
|
||||
|
||||
| Deltas | Baseline: safe close | Candidate: safe close | Baseline: visible + close | Candidate: visible + close |
|
||||
| ------ | -------------------- | --------------------- | ------------------------- | -------------------------- |
|
||||
| 16 | 116 [110–116] | 109 [109–109] | 912 [903–925] | 762 [751–764] |
|
||||
| 128 | 179 [169–179] | 159 [155–173] | 5,665 [5,596–5,808] | 4,369 [4,284–4,395] |
|
||||
| 512 | 4,122 [4,061–4,355] | 4,057 [3,868–4,604] | 23,013 [22,755–23,163] | 17,802 [17,422–18,879] |
|
||||
|
||||
The fixture emits its burst in 0–2 ms. At 512 deltas, the median Rust terminal
|
||||
emission delay changed from 16,706 to 11,814 ms. Controller cursor commits and
|
||||
committed event counts remain 24 / 136 / 520 at the three sizes; observed
|
||||
controller saves remain 42 / 154 / 540. The benchmark does not instrument Rust
|
||||
save counts or exact wire ACK counts and reports those as unknown.
|
||||
|
||||
The roughly four-second 512-delta close tail remains. The total median
|
||||
visibility-plus-close reduction is 22.6% at that size; this patch does not solve
|
||||
all cumulative controller/wire-ACK work. Drain and suspend receipt intervals
|
||||
can overlap, so they must not be added together as independent serial costs.
|
||||
|
||||
## Artifacts and reproduction
|
||||
|
||||
Implementation and fixture:
|
||||
|
||||
- `packages/paperclip-runner/src/live/runnerd-final-output-burst.benchmark.test.ts`
|
||||
- `packages/paperclip-runner/test/fixtures/fake-final-burst-codex-app-server.mjs`
|
||||
- Production change: `packages/paperclip-runner/runner/crates/runner-core/src/durable/runner.rs`, `poll_executor_events`.
|
||||
|
||||
Measured binary SHA-256 digests:
|
||||
|
||||
- Baseline: `e33d464cba6766becf9fb536182976c0359a78e4250301a5c86874f8212c9963`
|
||||
- Candidate: `8a61219d5b492b8bdff55600d25a8da818e5f66b095e68fbac40a8a9e7013370`
|
||||
|
||||
The baseline is retained locally at
|
||||
`/tmp/paperclip-final-burst-cargo.YoIBsw/baseline-paperclip-runnerd`; the candidate
|
||||
is `/tmp/paperclip-final-burst-cargo.YoIBsw/release/paperclip-runnerd`.
|
||||
These temporary binaries are not repository artifacts. The candidate used the
|
||||
optimized release profile in this isolated Cargo target, never the live target
|
||||
or staging script. The live staged binary retained the baseline digest after
|
||||
the comparison.
|
||||
|
||||
Exact local evidence filenames, under the ignored
|
||||
`.paperclip-runtime/chat-adapters-live/runner-output-burst-benchmark-20260908/`:
|
||||
|
||||
- `baseline.metrics.jsonl`: selected closed metric fields exported from captured
|
||||
`FINAL_BURST_BENCHMARK` stdout, execution session `62858`, 9/9 passed.
|
||||
- `candidate.metrics.jsonl`: the corresponding export from execution session
|
||||
`96555`, 9/9 passed.
|
||||
|
||||
These are metric exports, **not full shell/Vitest logs**. Full Vitest results
|
||||
were captured by the execution tool: baseline 103.48 s, candidate 83.97 s.
|
||||
|
||||
Run from `packages/paperclip-runner`:
|
||||
|
||||
```sh
|
||||
PAPERCLIP_FINAL_BURST_BENCHMARK=1 PAPERCLIP_FINAL_BURST_REPETITIONS=3 PAPERCLIP_FINAL_BURST_BINARY=/tmp/paperclip-final-burst-cargo.YoIBsw/baseline-paperclip-runnerd pnpm exec vitest run src/live/runnerd-final-output-burst.benchmark.test.ts
|
||||
PAPERCLIP_FINAL_BURST_BENCHMARK=1 PAPERCLIP_FINAL_BURST_REPETITIONS=3 PAPERCLIP_FINAL_BURST_BINARY=/tmp/paperclip-final-burst-cargo.YoIBsw/release/paperclip-runnerd pnpm exec vitest run src/live/runnerd-final-output-burst.benchmark.test.ts
|
||||
```
|
||||
|
||||
Without `PAPERCLIP_FINAL_BURST_BINARY`, the test selects the existing staged
|
||||
runner (or the existing debug runner if none is staged). It never builds one.
|
||||
Every invocation copies the selected binary into a private fixture directory,
|
||||
checks its SHA before and after, and uses an explicit empty Codex home and no
|
||||
provider credentials. Without the opt-in flag, all three cases are skipped.
|
||||
Repetitions are bounded to 1–5.
|
||||
|
||||
## Preserved invariants and checks
|
||||
|
||||
- All synthetic deltas arrive in exact order, with no loss or duplicates.
|
||||
- The declared semantic completion handler executes once.
|
||||
- Durable committed source sequences are contiguous, logical effects occur
|
||||
once, and runner/controller ACK cursors agree.
|
||||
- Safe close requires the exact six-field identity in durable suspended state
|
||||
plus a completed suspension command; the close deadline is unchanged.
|
||||
- Reopening the same run does not execute the semantic tool again.
|
||||
- A successor authority can reopen/read the same provider thread and close
|
||||
with its exact identity. **It does not execute a second provider turn**:
|
||||
the fixture's turn count remains one. Live consecutive-turn qualification is
|
||||
separate. The output names this `sameProviderAuthorityReopen` and explicitly
|
||||
reports `successorTurnExecuted: false`.
|
||||
- An oversized or identity-conflicting suffix acknowledges only the prior
|
||||
durable prefix. A failed durable save authorizes no ACK. An ACK failure
|
||||
retains replayable receipts; if commit and ACK both fail, the original commit
|
||||
error remains observable. No same-memory retry can treat an unsaved receipt
|
||||
as durable.
|
||||
|
||||
Regression evidence: the original loop failed three of four focused batch
|
||||
tests; the candidate passed all 18 durable-runner tests and all 217 Rust
|
||||
library tests. The expanded crash/replay test also covers changed event data
|
||||
after controller ACK removed the outbox copy. Independent review found no
|
||||
blocker. Runner no-emit TypeScript checking, fixture syntax, formatting, and
|
||||
`git diff --check` passed. The no-emit production configuration excludes test
|
||||
files; actual benchmark executions provide the test-path verification.
|
||||
|
||||
```sh
|
||||
cargo test --manifest-path runner/Cargo.toml --locked --offline --target-dir /tmp/paperclip-final-burst-cargo.YoIBsw -j 2 -p paperclip-runner-core --lib durable::runner::tests
|
||||
cargo test --manifest-path runner/Cargo.toml --locked --offline --target-dir /tmp/paperclip-final-burst-cargo.YoIBsw -j 2 -p paperclip-runner-core --lib -- --test-threads=2
|
||||
node node_modules/typescript/bin/tsc -p tsconfig.json --noEmit
|
||||
```
|
||||
|
||||
Next validation is a root-coordinated staged build and real native chat
|
||||
comparison. Any future controller ACK/persistence optimization needs its own
|
||||
crash-boundary and replay proof; this change provides no authority to relax
|
||||
durable receipt, ordering, or suspension requirements.
|
||||
|
|
@ -0,0 +1,380 @@
|
|||
# Chat implementation landing checkpoint — September 9, 2026
|
||||
|
||||
This working note can be deleted after both landing PRs merge and the remaining
|
||||
live hardening is represented by its own follow-up PR.
|
||||
|
||||
## Durable snapshot
|
||||
|
||||
- Snapshot revision: `007399bcd207f33aee7b62d14cf7a854cb979eca`.
|
||||
- Snapshot tree: `5ba0226bedf205102b01ab7fae5d8bdf98cb832f`.
|
||||
- Immutable local recovery branch: `codex/chat-adapters-snapshot-20260909`.
|
||||
- Parent: `9afdf3232d2ac781ce4af05350129a6a8c7e2eb2`.
|
||||
- Captured all 29 modified/new implementation and qualification-document paths
|
||||
using an alternate Git index. All 29 working-file hashes matched the snapshot.
|
||||
The original checkout's HEAD and empty staging area were unchanged.
|
||||
- Landing worktree: `/Users/dotta/paperclipai/branches/chat-adapters-landing-20260909`.
|
||||
- Initial landing branch: `codex/chat-adapters-landing-20260909`.
|
||||
- Origin master at snapshot: `5acf56658bff7eeb12438a6fdcae5f4d2fe1e90e`.
|
||||
- Ignored live runtime, credentials, databases, generated packages, and the
|
||||
protected runner binary were not added. Changed/new files passed the scoped
|
||||
credential-marker scan. This is not a claim of a full repository secret audit.
|
||||
|
||||
## Separate lanes
|
||||
|
||||
The user explicitly authorized the separate landing worktree and superseded the
|
||||
earlier no-new-worktree/no-PR-tending constraints for this lane. James owns
|
||||
reconciliation, exactly two coherent stacked PRs under 500 changed files each,
|
||||
fresh exact-head Greptile 5/5, required checks, and dependency-ordered merges.
|
||||
The existing PR is https://github.com/paperclipai/paperclip/pull/13038; it had 526
|
||||
changed files and conflicts at this checkpoint. Preserve its review context
|
||||
where practical. Do not merge based on old review scores or narrow local tests.
|
||||
|
||||
James exclusively owns remote `codex/chat-adapters` updates while reorganizing
|
||||
that PR. The original local branch must not push over the landing heads. Do not
|
||||
modify the original checkout or live runtime from the landing worktree.
|
||||
|
||||
The root, Epicurus, and Boole continue live stress qualification and subsequent
|
||||
hardening in the original checkout. Changes after the snapshot remain separate.
|
||||
After both merges, reconcile the ongoing branch with merged master, preserve
|
||||
newer fixes, and open a follow-up PR without reintroducing landed changes.
|
||||
|
||||
## Verification boundary
|
||||
|
||||
Snapshot evidence: Board attachment cohort 417/417; blocked-continuation cohort
|
||||
36/36; Rust durable-runner cohort 36/36; plain server/UI/runner TypeScript checks
|
||||
passed; token gates clean. The full transport cohort was still running. These
|
||||
are focused checks, not current-head full repository or landing CI verification.
|
||||
|
||||
Server 78 remained running on port 3137. Its loaded version was
|
||||
`2026.831.0+623.git.ea528f44c`; a dynamically read Git HEAD is not proof that newer
|
||||
source was deployed. No live restart or protected binary replacement occurred
|
||||
while creating the snapshot. Historical quarantined recovery evidence remains
|
||||
untouched.
|
||||
|
||||
## Subsequent checkpoint — 13:52 UTC
|
||||
|
||||
Base PR [#13092](https://github.com/paperclipai/paperclip/pull/13092) is open
|
||||
with 45 changed files. Master reconciliation has exposed additional native
|
||||
goal/integrity and PRP-v2 warm-authorization/state-retention defects. James owns
|
||||
their landing-only regressions; neither the initial PR head nor historical
|
||||
Greptile reviews certify the corrected head. The current-master warm-upgrade
|
||||
compatibility boundary must be explicit, not hidden by a fail-closed test.
|
||||
|
||||
Root's test-only `5232fb22b` is available for the second PR. The new Board
|
||||
uncertain-write and late-semantic-result hardening remain post-snapshot work.
|
||||
A newly observed live Discord close/recovery loop must be fixed and qualified
|
||||
before the experimental connector PR merges; it is not cosmetic follow-up.
|
||||
Maya is temporarily paused to contain that loop. No deployment occurred.
|
||||
|
||||
## Subsequent checkpoint — 14:02 UTC
|
||||
|
||||
The base is now 47 files at `3e7289cd4` (James owns publication and exact-head
|
||||
checks). Its prior head's Greptile 5/5 does not certify this head. Full workspace
|
||||
build passed in the isolated landing worktree; full tests/checks remain pending.
|
||||
The post-snapshot semantic-result fix is included in the base via its exact
|
||||
four-file delta, not a duplicate cherry-pick of the full snapshot-containing
|
||||
commit. Root's local commits are `c76988f93` (runner) and `ae21fd9e2` (Board).
|
||||
|
||||
The real process-replacement test proves a **v2-capable current artifact** first
|
||||
leased as v1 can retire its exact owner and negotiate v2 on fresh bootstrap,
|
||||
preserving native cached state before a warm attach. It does not prove an old
|
||||
binary upgrade: the existing restart closure retains its original artifact.
|
||||
Same-lease reconnect remains v1; adopted owners have no automatic upgrade path.
|
||||
Do not advertise this internal recovery proof as a new operator upgrade API.
|
||||
|
||||
Board qualification finished 350 focused units and 11 actual browser journeys.
|
||||
Runner release qualification finished 27 composed tests, in addition to 227
|
||||
serial source tests. The close/recovery defect has two clean failing regressions
|
||||
and remains a merge gate for the top PR. All live runs remain deliberately
|
||||
paused. The original live binary and lockfile are unchanged.
|
||||
|
||||
## Subsequent checkpoint — 14:35 UTC
|
||||
|
||||
The current published base is `46ef7ef03ca35a47d6ac2be9e2dd497b137d3b70`,
|
||||
44 changed files. Its exact-head Greptile score is 3/5; the prior 5/5 scores
|
||||
do not satisfy the merge gate. James is addressing the concrete review
|
||||
findings and current-master compatibility fixtures in the landing worktree.
|
||||
The full runner suite at that head was 1,881 passed, six failed, ten skipped;
|
||||
focused corrected fixtures do not replace the required fresh full-suite run.
|
||||
Master has advanced through `35fdc0c66`, including durable task recovery work
|
||||
that the top PR must preserve rather than overwrite with the older snapshot.
|
||||
|
||||
The original checkout's full chat integration suite is now **860/860 passed**
|
||||
on fresh database `chat_snapshot_full_20260909_root06`, through test/copy fixes
|
||||
in `02dc80d1e`. This is not a landing exact-head or full-workspace result.
|
||||
All earlier failed runs remain recorded in the qualification notes.
|
||||
|
||||
The last close/recovery crash window has a genuine failing regression:
|
||||
restoring the old blanket native-recovery exemption dispatches one provider
|
||||
attempt after a committed close, where zero are allowed. The replacement
|
||||
records exact-run `required`/`admitted` admission evidence in the server-owned
|
||||
runner profile and preserves historical, already-admitted recovery behavior.
|
||||
Nine focused cases pass; the final full recovery suite and final review are
|
||||
still pending. These tests compose real native preparation and the actual
|
||||
restart classifier, not an operating-system process crash.
|
||||
|
||||
Server 78 remains unchanged and Maya remains paused. The qualified release
|
||||
runner has been copied to a private, read-only QA path but has not been
|
||||
activated. A fresh database backup and controlled cutover precede the next
|
||||
live question/form/close and attachment-fallback qualification.
|
||||
|
||||
## Subsequent checkpoint — 15:18 UTC
|
||||
|
||||
Base `335b2ee52709afb3885d4d6ebb2a3ece4b5864d6`, 47 changed files,
|
||||
received a fresh Greptile **5/5**, clean security review and fully successful CI
|
||||
run `34367194680`. Its complete local runner suite passed **1,888 tests**, with
|
||||
10 preexisting skips. The whole release Rust workspace passed with serial test
|
||||
scheduling and unchanged deadlines. A default-parallel attempt still exceeded
|
||||
the descendant-lineage fixture's five-second deadline under load and remains
|
||||
recorded; it was not hidden by the isolated or serial pass.
|
||||
|
||||
Master subsequently advanced to `82f662656` (#13093–13095, #13097). The base
|
||||
now has a runner-transport merge conflict. The landing agent will finish
|
||||
collecting its running broad 335 test result before changing source, then
|
||||
reconcile and requalify the new head. The 335 approvals/checks do not authorize
|
||||
merging a later head without fresh verification.
|
||||
|
||||
Top reconciliation must preserve master's execution recovery ordering and the
|
||||
snapshot's physical-owner/usage fences. In particular, a Board reconciliation
|
||||
on a chat-bound task cannot create both a generic pending successor and a
|
||||
separate authorized failed-chat retry. The proposed typed single-owner receipt
|
||||
keeps current chat source/access checks and existing idempotent retry identity;
|
||||
non-chat behavior stays unchanged. Joined regression evidence is required.
|
||||
|
||||
Root deployed server 79 from local `3f2387073` and resumed Maya. Discord's
|
||||
native question/choice/free-text flow passed live; Slack's true queue and native
|
||||
Stop/fresh-follow-up passed. Fresh Discord close exposed an old-definition
|
||||
registration incompatibility; GitHub's private-file Board fallback exposed an
|
||||
unwanted passive-wait continuation and a misleading already-bound-file send
|
||||
error. Repairs and final tests are in progress in the original checkout and
|
||||
have not been pushed over the landing branch. Exactly two coherent PRs under
|
||||
500 files and dependency-order landing remain the required structure.
|
||||
|
||||
## Subsequent checkpoint — 17:39 UTC
|
||||
|
||||
The user merged runner prerequisite [#13092](https://github.com/paperclipai/paperclip/pull/13092)
|
||||
and explicitly required **two remaining chat PRs**; the runner does not count
|
||||
toward those two. The chat foundation is [#13100](https://github.com/paperclipai/paperclip/pull/13100),
|
||||
143 files at `1c3c34c9b5d8dcc0a732beefcb683712b1d9bf8b`. The integration remains
|
||||
[#13038](https://github.com/paperclipai/paperclip/pull/13038), 370 files at
|
||||
`21d3f81f990e419634df765043795e328dd8f6b9`, stacked on the foundation. Neither
|
||||
has merged. The foundation does not mount routes or activate providers.
|
||||
|
||||
Foundation exact-head CI `34381883937` is fully green, including required
|
||||
`ci / verify` and `ci / e2e`, build, release canary, workspace/general suites,
|
||||
and serialized server suites. The exact isolated local workspace typecheck
|
||||
also passes; its full local test/build chain is still running. Earlier
|
||||
historical-migration fixture failures are preserved in the evidence ledger;
|
||||
the corrected four-file database cohort passes 27/27. Tenant/delete/drift
|
||||
matrix passes 2/2 and runtime/adapter lifecycle tests pass 119/119. Fresh
|
||||
exact-head Greptile review is still missing after manual requests; resolved
|
||||
prior findings and the old score do not satisfy that gate.
|
||||
|
||||
Integration CI `34381886310` passed every job except general-server shard 2/5
|
||||
and its dependent aggregate. That shard passed 2,615 tests and failed four
|
||||
warm-session checkpoint fixtures. The actual failure was an exact-value
|
||||
assertion: the rejected persisted checkpoint is `null`, not `undefined`.
|
||||
The assertion exception entered failure projection against a partial mock
|
||||
database, masking itself as `runner.insert is not a function`. A temporary
|
||||
diagnostic service mock exposed the original assertion and was then removed.
|
||||
Landing-only test commit `66f16f244` changes only that refusal assertion and its
|
||||
explanatory comment. The focused matrix passes 28/28, complete executor file
|
||||
313/313, and plain server types pass; production authority is unchanged.
|
||||
Both failed logs remain available in the ignored qualification directory.
|
||||
|
||||
Master advanced to `8cfd30fb0`, including composer Stop and task-control
|
||||
simplification. An isolated three-way composition preserves those changes
|
||||
alongside awaited Board submissions, retained uncertain drafts, exact private
|
||||
comment attribution, and cache invalidation. Shared build, UI types and six
|
||||
affected UI suites pass 395/395 on that preview. It is not yet the remote
|
||||
integration head or full integration qualification. The final integration
|
||||
must be updated after foundation merge and receive fresh gates again.
|
||||
|
||||
Both current chat PR file lists were checked: no wireframe images or HTML
|
||||
galleries remain. The integration includes only three production provider
|
||||
brand SVGs. Separately, a delayed first-seen pre-close source admission path
|
||||
is under bounded service investigation in the source checkout; integration
|
||||
merge is held pending that result. Historical live and full-service passes
|
||||
do not certify that new edge, and no live checkout, dependency, runner binary,
|
||||
or provider configuration was modified by this landing work.
|
||||
|
||||
## Subsequent checkpoint — 18:15 UTC
|
||||
|
||||
Foundation remains 143 files at `1c3c34c9b5d8dcc0a732beefcb683712b1d9bf8b`.
|
||||
Its required exact-head CI is green, but the only current Greptile status still
|
||||
says the 143-file change exceeds the automatic 100-file limit. Manual review
|
||||
requests at 17:16 and 17:30 have not produced a fresh review. The historical
|
||||
3/5 on `29c48d25` is not current approval; no merge or repeated request spam
|
||||
has bypassed the gate.
|
||||
|
||||
The isolated exact-head local monolithic run stopped at a database fixture's
|
||||
embedded-Postgres initialization failure: 107 database tests passed, 25 were
|
||||
skipped by support probes, and one failed during initialization. The unchanged
|
||||
full database cohort then passed 133/133 with serial file scheduling. The
|
||||
resumed Codex adapter suite found a separate fixed-run-ID temporary-directory
|
||||
collision (expected one staged home, found four). Its unchanged focused test
|
||||
passes in a fresh owned temporary directory with a Git-discovery ceiling.
|
||||
The first temporary-directory-only retry inherited the enclosing repository
|
||||
and failed a Git fetch; that unsuccessful harness attempt is retained too.
|
||||
Other workspace groups pass; serialized local suites and build are still
|
||||
running. None of these resumed checks relabels the original monolithic run
|
||||
as green. Logs are retained under the ignored `foundation-verify-PqC0e6`
|
||||
qualification directory.
|
||||
|
||||
The integration privately includes root's `752d52a00` intake guard and
|
||||
`d6724e057` shared chronology repair, including exact JavaScript-trim parity
|
||||
for accepted commands. Its file count is now 371 against the foundation.
|
||||
The root's final full-service and browser repeats remain separate pending
|
||||
gates; earlier live or full-service passes do not certify the new chronology
|
||||
edge. No wireframe images or HTML galleries were added; the only changed
|
||||
image assets remain three production provider-brand SVGs. Historical
|
||||
wireframe generator source and its archive note are not image artifacts.
|
||||
|
||||
The physical master-UI composition passes all 5,853 UI tests, in addition to
|
||||
the previously recorded 395 affected tests and UI types. Its initial browser
|
||||
cohort passed 32 chat cases but failed the unchanged process-adapter composer
|
||||
Stop case. An owned SIGTERM exit had `exitCode: null`, which the executor
|
||||
treated as zero while cancellation was still awaiting termination. The run
|
||||
could therefore become Succeeded before the cancellation compare-and-set.
|
||||
|
||||
The narrow fix applies only to the process adapter. Overlapping Stop calls
|
||||
join one owned in-memory attempt; executor settlement waits until that attempt
|
||||
and its cancellation write settle. Failed-attempt evidence separately prevents
|
||||
a graceful SIGTERM handler's zero exit from being called success, while a
|
||||
later Stop can retry a still-owned live child. Existing terminal database
|
||||
winners remain authoritative. Native adapters and other legacy adapters are
|
||||
unchanged; this adds no durable cancellation or provider authority.
|
||||
|
||||
Actual-process tests cover signal and graceful exits, adapter exceptions,
|
||||
termination/write failures, duplicate callers after child-map removal, delayed
|
||||
results, a first failed Stop followed by a successful retry, and independent
|
||||
clean-completion winners. Two graceful-failure counterexamples were retained
|
||||
as genuine REDs before repair. The final selected Stop/paused-wait cohort
|
||||
passes 29/29. The preceding full recovery run passed 246/248; its two paused
|
||||
fixtures mixed PostgreSQL microsecond defaults with a later rounded JS run
|
||||
clock, making the supposed source occur after admission. Explicit ordered
|
||||
fixture timestamps preserve all production guards and negative assertions.
|
||||
The final full recovery file passes 252/252 (124.53s) on a fresh database,
|
||||
and plain server types pass. Independent review is clear at source SHA
|
||||
`f23b50982a750a0fd8cfe1c79cf40eaeca7afa3456d377098d9aa20c5bf975d5`.
|
||||
|
||||
The final unchanged process browser journey passes 1/1 (50.1s test, 1.0m total)
|
||||
on isolated port 3233: queued comment, actual composer Stop, refresh and
|
||||
maintenance hold, explicit resume, subtree pause/cancel, and an unaffected
|
||||
completed child. The inspected screenshot shows Cancelled and Stopped, with
|
||||
the queued comment retained. This is deterministic process-adapter proof,
|
||||
not the optional native fake-Codex case or a live-provider Stop claim.
|
||||
The process fixture has no assistant transcript, so its Waiting for transcript
|
||||
copy is not evidence about a model conversation. Mis-selected zero-test grep
|
||||
attempts and a pre-test shared-memory allocation failure are retained as
|
||||
harness failures, not product REDs or passes. Only positively owned retired
|
||||
fixture clusters were restarted and normally stopped to reclaim their own
|
||||
IPC segments; database directories remain, all other clusters and global
|
||||
settings are untouched. Detailed logs and the final trace/screenshot remain
|
||||
in ignored `integration-master-ui-Joeb8O` qualification artifacts.
|
||||
|
||||
### Foundation merged; integration final-base qualification
|
||||
|
||||
Foundation #13100 merged at 18:49:13 UTC as
|
||||
`6abeb67334348dcb6fde2d591a27ffc7efc7118d`, after required exact-head CI,
|
||||
current approval, resolved prior threads, and a fresh Greptile 5/5 explicitly
|
||||
naming `1c3c34c9b5d8dcc0a732beefcb683712b1d9bf8b`. Its isolated full build
|
||||
also completed successfully. Serialized local coverage completed across
|
||||
144 files and 2,179 tests: the retained OpenCode environment timeout passes
|
||||
unchanged with an owned empty XDG configuration; the remaining 24 files
|
||||
pass 297/297. These resumed runs do not erase the earlier monolithic failures.
|
||||
|
||||
Integration head `ac71491df` received an exact-head 4/5 review. Its only
|
||||
finding alleged same-agent unrelated-run toast suppression. The actual
|
||||
producer includes `runId`; the suppression helper returns exact run membership
|
||||
before its agent-only fallback, and the toast builder requires `runId`.
|
||||
Greptile explicitly withdrew the finding after this call-chain evidence.
|
||||
Six mounted WebSocket-to-cache-to-toast regressions on unchanged production
|
||||
source pass, with both LiveUpdatesProvider files 53/53 and UI types passing.
|
||||
The first added-test attempt was 52/53 because it incorrectly expected a
|
||||
success toast; existing policy deliberately excludes successful-run toasts.
|
||||
That fixture expectation was corrected without changing notification policy.
|
||||
|
||||
The ac714 CI Build job failed during runner verification before building:
|
||||
1,897 tests passed, three failed, and three existing tests were skipped.
|
||||
One retained-maintenance case hit the fixed terminal-result ACK deadline
|
||||
with an older durable event backlog. Two later cases inherited that failed
|
||||
fixture's intentionally sticky cleanup quarantine because their backend
|
||||
domain names were shared. A causal regression reproduces that contamination;
|
||||
unique immutable per-row fixture names let an independent case start while
|
||||
the original domain remains quarantined. Its focused test and runner types
|
||||
pass. No production quarantine reset or deadline relaxation is introduced.
|
||||
The original ACK timeout remains a separate unresolved gate at this checkpoint.
|
||||
|
||||
The second chat PR is being rebased onto the actual merged foundation and
|
||||
newer master changes. The shared transport union must retain `chat_sdk`, and
|
||||
the deferred-wake test import union preserves both upstream and chat cases.
|
||||
The post-base head still requires complete CI and a fresh exact-head 5/5;
|
||||
neither this foundation merge nor the withdrawn finding authorizes the
|
||||
integration merge.
|
||||
|
||||
### Final-base component qualification — 19:38 UTC
|
||||
|
||||
The private integration candidate is
|
||||
`090cde5144b2eb5119d91336263bd462fe28d98e`, 379 changed files against
|
||||
merged foundation/master `6abeb67334348dcb6fde2d591a27ffc7efc7118d`.
|
||||
Its production bytes match `66cab99df`, whose isolated physical checkout
|
||||
passed the full workspace build. Only reviewed portable test fixtures and
|
||||
qualification records changed afterward. The protected original checkout,
|
||||
lockfile, installed dependencies, runner binary, and live server were not
|
||||
changed. Neither chat PR contains generated wireframe images or HTML galleries;
|
||||
the three integration image paths are production provider-brand SVGs.
|
||||
|
||||
The primary ACK-loss repair now passes the actual delayed-fsync counterexample,
|
||||
19 retained-maintenance cases, 80 controller cases, and package types. It
|
||||
replays only an exact completed terminal receipt within the same owned
|
||||
maintenance invocation. It joins retired connection processing before reading
|
||||
evidence, rechecks authority after retirement, and cannot launch a provider.
|
||||
The earlier same-domain fixture quarantine cascade is independently fixed by
|
||||
unique per-row fixture identities, not by resetting production quarantine.
|
||||
|
||||
The first complete post-base runner attempt used the local default 17 Vitest
|
||||
workers and retained 14 failures, 1,890 passes, ten existing skips, and five
|
||||
reported unhandled errors. Two repeated failures were Darwin path aliases in
|
||||
fixture expectations and filesystem hooks. Canonical fixture paths preserve
|
||||
the original integrity and failure assertions. A separate fixture port
|
||||
collision is handled only during bounded, ownership-safe preparation before
|
||||
staging once. The two unchanged startup/installed-dependency timeout cases pass
|
||||
in isolated files; their original exact scheduling causes remain unproved.
|
||||
No timeout was increased and no assertion or security gate was skipped.
|
||||
|
||||
The subsequent exact-source check used `VITEST_MAX_WORKERS=1`. All 38 Node
|
||||
contracts, 1,906 executed Vitest tests, and replay goldens passed; Vitest took
|
||||
325.96 seconds total (308.75 seconds tests). Its ten unchanged exclusions are
|
||||
three opt-in benchmarks and seven Linux-only executable/guardian cases. This
|
||||
is explicit local isolated qualification, not a claim about GitHub's worker
|
||||
count or default-CI behavior.
|
||||
|
||||
That same command then stopped with a Rust failure under the original default
|
||||
Rust test concurrency. The descendant-lineage fixture missed its first
|
||||
five-second completion check. Its retained state had processed 255 of 300
|
||||
descendants, still active, before the terminal event. Two bounded 128-event
|
||||
polls account for that prefix; persistence includes file/directory fsync.
|
||||
No restoration or capacity assertion had yet run. The unchanged complete test
|
||||
passes alone in 3.64 seconds. This demonstrates progress at the limit, not a
|
||||
uniquely identified storage or scheduling bottleneck. All Rust source and test
|
||||
files are byte-identical to merged master, and an earlier instance of this
|
||||
deadline failure was already recorded above.
|
||||
|
||||
The entire unchanged release Rust workspace then passed with explicit
|
||||
`--test-threads=1`: 533 top-level tests plus two executed subprocess-helper
|
||||
checks, with no failures. The two helper declarations are ignored in the
|
||||
parent harness because their owning tests invoke them explicitly. The
|
||||
conformance check passed 1/1, replay parity passed 11/11, and the required
|
||||
actual runner-to-HTTP authority suite passed 870/870 across three files.
|
||||
These resumed component passes do not turn the original halted `check:all`
|
||||
invocation into a pass. Its failed log remains alongside all subsequent logs
|
||||
in the ignored `integration-base-verify-YAhDBQ` stage.
|
||||
|
||||
The isolated checkout stayed clean, its lockfile retained SHA-256
|
||||
`822ecb8c7463689b2b6a09f5d262b85ae99a39b06e6461813e14410e62b2b8b6`,
|
||||
and its privately built runner retained SHA-256
|
||||
`ea9b3abfe98b5ba752ad492a1a6e413e4f6afd1e8b5da812902999e334f1452e`.
|
||||
The next remote update must obtain its own required CI and fresh exact-head
|
||||
Greptile 5/5 before integration merge. The previous 4/5 finding was withdrawn;
|
||||
that withdrawal is not a fresh 5/5 for the new head.
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { providerScreens } from "./platform-wireframe-data.mjs";
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url));
|
||||
const out = join(root, "wireframes-v2");
|
||||
mkdirSync(out, { recursive: true });
|
||||
|
||||
const esc = (value) => String(value)
|
||||
.replaceAll("&", "&").replaceAll("<", "<")
|
||||
.replaceAll(">", ">").replaceAll('"', """);
|
||||
const tx = (x, y, value, size = 14, fill = "#000", extra = "") =>
|
||||
`<text x="${x}" y="${y}" font-size="${size}" fill="${fill}" stroke="none" ${extra}>${esc(value)}</text>`;
|
||||
const ln = (x1, y1, x2, y2, extra = "") => `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" ${extra}/>`;
|
||||
const rc = (x, y, w, h, extra = "") => `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="6" ${extra}/>`;
|
||||
const circle = (x, y, r, extra = "") => `<circle cx="${x}" cy="${y}" r="${r}" ${extra}/>`;
|
||||
|
||||
function baseSvg(width, height, body) {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5"><rect width="${width}" height="${height}"/>${body}</svg>`;
|
||||
}
|
||||
|
||||
function textLines(x, y, lines, size = 12, fill = "#666", gap = 20, extra = "") {
|
||||
return lines.map((line, index) => tx(x, y + index * gap, line, size, fill, extra)).join("\n");
|
||||
}
|
||||
|
||||
function button(x, y, w, label, primary = false) {
|
||||
return `${rc(x, y, w, 48, primary ? 'fill="#000"' : 'fill="#fff"')}${tx(x + w / 2, y + 30, label, 14, primary ? "#fff" : "#000", 'text-anchor="middle" font-weight="600"')}`;
|
||||
}
|
||||
|
||||
function status(x, y, label, state = "Ready") {
|
||||
return `${circle(x, y - 4, 5, 'fill="#e6e6e6"')}${tx(x + 14, y, label, 12, "#000")}${tx(x + 178, y, state, 12, "#666", 'text-anchor="end"')}`;
|
||||
}
|
||||
|
||||
function annotations(regions, mobile = false) {
|
||||
return `<g data-region="annotations">${regions.map((region, index) => {
|
||||
const radius = mobile ? 9 : 12;
|
||||
return `${rc(region.x, region.y, region.w, region.h, 'fill="none" stroke="#d33" stroke-dasharray="6 4"')}${circle(region.x, region.y, radius, 'fill="#fff" stroke="#d33" stroke-dasharray="4 2"')}${tx(region.x, region.y + 4, index + 1, 12, "#d33", 'text-anchor="middle" font-weight="700"')}`;
|
||||
}).join("\n")}</g>`;
|
||||
}
|
||||
|
||||
function globalSidebar() {
|
||||
const items = ["New Task", "Search", "Dashboard", "Inbox", "Tasks", "Projects", "Routines", "Artifacts", "Agents", "Skills", "Connectors", "Audit"];
|
||||
return `<g data-region="global-navigation">${tx(24, 38, "Paperclip", 20, "#000", 'font-weight="700"')}${items.map((item, index) => {
|
||||
const y = 78 + index * 46;
|
||||
return `${item === "Connectors" ? rc(12, y - 28, 216, 38, 'fill="#e6e6e6"') : ""}${circle(32, y - 10, 6, 'fill="#e6e6e6"')}${tx(52, y - 5, item, 14, item === "Connectors" ? "#000" : "#666", item === "Connectors" ? 'font-weight="600"' : "")}`;
|
||||
}).join("\n")}${tx(24, 744, "Acme Company", 14, "#000", 'font-weight="600"')}${tx(24, 772, "Dana · Admin", 12, "#666")}${ln(240, 0, 240, 800)}</g>`;
|
||||
}
|
||||
|
||||
function topbar(crumb) {
|
||||
return `<g>${ln(240, 60, 1280, 60)}${tx(264, 36, crumb, 14, "#666")}${circle(1240, 30, 16, 'fill="#e6e6e6"')}</g>`;
|
||||
}
|
||||
|
||||
function setupContext(provider) {
|
||||
return `<g>${tx(264, 96, "CONNECTORS", 12, "#666", 'font-weight="600"')}${rc(252, 116, 216, 40, 'fill="#e6e6e6"')}${tx(280, 142, "Connect", 14, "#000", 'font-weight="600"')}${tx(280, 190, provider, 14, "#666")}${tx(280, 238, "External setup", 14, "#666")}${ln(480, 60, 480, 800)}</g>`;
|
||||
}
|
||||
|
||||
function detailContext(provider, active = "Settings") {
|
||||
const items = ["Overview", "Settings", "Access", "Conversations", "Activity"];
|
||||
const label = provider === "Microsoft Teams" ? "Teams" : provider;
|
||||
return `<g>${tx(264, 94, "‹ All connectors", 12, "#666")}${circle(280, 132, 18, 'fill="#e6e6e6"')}${tx(308, 138, `Maya on ${label}`, 14, "#000", 'font-weight="700"')}${items.map((item, index) => `${item === active ? rc(252, 168 + index * 48, 216, 40, 'fill="#e6e6e6"') : ""}${tx(280, 194 + index * 48, item, 14, item === active ? "#000" : "#666", item === active ? 'font-weight="600"' : "")}`).join("\n")}${ln(480, 60, 480, 800)}</g>`;
|
||||
}
|
||||
|
||||
function heading(screen, step = "") {
|
||||
return `${step ? tx(504, 90, step, 12, "#666", 'font-weight="600"') : ""}${tx(504, step ? 124 : 108, screen.title, 28, "#000", 'font-weight="700"')}${tx(504, step ? 152 : 136, screen.subtitle, 14, "#666")}`;
|
||||
}
|
||||
|
||||
const setupData = {
|
||||
Slack: {
|
||||
bot: "Maya → Slack bot @maya", identity: "Workspace app · one bot identity",
|
||||
delivery: "Direct verified webhook", deliveryNote: "Advanced: Paperclip relay or Slack Socket Mode",
|
||||
secrets: ["Bot/OAuth token •••• 7K2M", "Signing secret •••• C19Q"],
|
||||
steps: ["Create app from generated manifest", "Install app to workspace or Grid org", "Return token/secret or finish OAuth", "Invite @maya to allowed channels"],
|
||||
verify: [["Bot + workspace", "Ready"], ["Signed event", "Ready"], ["Scopes + events", "Ready"], ["Channel membership", "Test next"]],
|
||||
action: "Verify Slack connection"
|
||||
},
|
||||
GitHub: {
|
||||
bot: "Maya → maya-paperclip[bot]", identity: "Chat purpose · GitHub App recommended",
|
||||
delivery: "Signed GitHub webhook", deliveryNote: "Advanced: GitHub Enterprise Server API URL",
|
||||
secrets: ["App ID 184205", "Private key •••• PEM", "Webhook secret •••• 93FW"],
|
||||
steps: ["Create GitHub App from checklist", "Grant Issues + PR write; Metadata read", "Subscribe to comment/review events", "Install on selected repositories"],
|
||||
verify: [["Signature ping", "Ready"], ["Bot self ID", "Ready"], ["Events", "Ready"], ["3 repositories", "Selected"]],
|
||||
action: "Verify GitHub App"
|
||||
},
|
||||
"Microsoft Teams": {
|
||||
bot: "Maya → Teams app Maya", identity: "Bot + app package · tenant installation",
|
||||
delivery: "Public messaging endpoint", deliveryNote: "Client secret or federated identity · not both",
|
||||
secrets: ["App ID •••• 9B2A", "Client secret •••• 18JD", "Tenant ID •••• 7F01"],
|
||||
steps: ["Run Teams CLI create with this endpoint", "Choose tenant mode and auth method", "Get install link or app package", "Install to personal/team/group scope"],
|
||||
verify: [["Entra + bot", "Ready"], ["Manifest", "Ready"], ["Endpoint", "Ready"], ["Tenant install", "Admin action"]],
|
||||
action: "Verify Teams installation"
|
||||
},
|
||||
Telegram: {
|
||||
bot: "Maya → Telegram @maya_helper_bot", identity: "Dedicated BotFather bot",
|
||||
delivery: "Verified webhook", deliveryNote: "Polling is local-development only",
|
||||
secrets: ["Bot token •••• A8KQ", "Webhook secret •••• H92P"],
|
||||
steps: ["Create bot and identity in @BotFather", "Keep privacy on; allow group joining", "Set webhook URL + secret token", "Add bot to intended chats/topics"],
|
||||
verify: [["getMe identity", "Ready"], ["Delivery mode", "Webhook"], ["Pending updates", "0"], ["Test chat", "Send next"]],
|
||||
action: "Verify Telegram bot"
|
||||
}
|
||||
};
|
||||
|
||||
const settingsData = {
|
||||
Slack: {
|
||||
reach: ["Workspace · Acme", "#customer-support · Invited", "#product-feedback · Invited", "DMs · On"],
|
||||
boundary: ["Root @maya → Slack thread", "One thread ↔ one Paperclip issue", "Bound replies need no mention"],
|
||||
capabilities: ["Agent Sessions + native stream · On", "Block Kit actions + modals · On", "Files + emoji/reactions · On", "Slash commands · Off", "Ephemeral denials · On"],
|
||||
security: ["OAuth workspace install", "Signature · Healthy", "Token rotation · Supported", "Socket Mode · Off"],
|
||||
fallback: "Missing scope → disable feature + Reinstall with scope"
|
||||
},
|
||||
GitHub: {
|
||||
reach: ["acme/api · Installed", "acme/web · Installed", "acme/legacy · Excluded", "GitHub.com"],
|
||||
boundary: ["Issue or PR conversation ↔ issue", "Review comment thread ↔ separate issue", "Discussions · Not in launch"],
|
||||
capabilities: ["Mention activation · On", "Receipt reaction · On", "One edited GFM progress comment", "Files → Paperclip links", "Labels/trusted authors · Advanced"],
|
||||
security: ["GitHub App installation", "Webhook signature · Healthy", "Self-message suppression · Ready", "Code/tool access · Separate connection"],
|
||||
fallback: "No stream/buttons/modals/DM → GFM text + Paperclip URL"
|
||||
},
|
||||
"Microsoft Teams": {
|
||||
reach: ["Tenant · Acme", "Support team / General · Allowed", "Personal scope · On", "Group chats · On"],
|
||||
boundary: ["Channel post + replies ↔ one issue", "DM/group chat ↔ active issue", "New task explicitly rebinds linear chat"],
|
||||
capabilities: ["Mention-only · On", "RSC all messages/history · Off", "Adaptive Cards + task modules · On", "DM native stream · On", "Group/channel buffered output"],
|
||||
security: ["Single tenant · Acme", "Federated identity · Healthy", "User.Read.All · Not granted", "DM history admin grant · Off"],
|
||||
fallback: "No RSC → mention on each undelivered reply; targeted → DM/text"
|
||||
},
|
||||
Telegram: {
|
||||
reach: ["Support group · Allowed", "Forum topic 381 · Allowed", "DMs · On", "Privacy mode · On"],
|
||||
boundary: ["DM → one active issue", "/new or New task → fresh issue", "Group @maya/reply; forum topic stable"],
|
||||
capabilities: ["Post/edit cadence · 3.1s group", "Native drafts in DMs · Off", "Inline buttons + URLs · On", "Files/media groups · On", "Ephemeral/modal/select · Unsupported"],
|
||||
security: ["Verified webhook · Healthy", "allowed_updates · Restricted", "Flood control · Normal", "Bot-to-bot routes · Off"],
|
||||
fallback: "Privacy-on unrelated traffic ignored; denial → reply/DM + link"
|
||||
}
|
||||
};
|
||||
|
||||
const interactionData = {
|
||||
Slack: [
|
||||
["Human", "Root: @maya investigate refund timeout", "Fresh root without @maya is ignored"],
|
||||
["Ingress", "Verify signature · persist · ack < 3s", "Deduplicate event_id; resolve Ari + channel"],
|
||||
["Binding", "Reply under root; claim Slack thread_ts", "Create one PAP issue assigned to Maya"],
|
||||
["Turns", "Thread replies, files, buttons, modal", "Reauthorize every actor/action; queue overlap"],
|
||||
["Output", "Native stream/edits + Stop → final", "Safe projection only; publication ID recorded"]
|
||||
],
|
||||
GitHub: [
|
||||
["Human", "@maya in issue, PR, or review comment", "Existing GitHub object supplies the thread"],
|
||||
["Ingress", "Verify X-Hub-Signature-256 + delivery", "Resolve installation, repository, and actor"],
|
||||
["Binding", "Object/thread key ↔ one PAP issue", "PR conversation ≠ inline review thread"],
|
||||
["Turns", "Comments continue; bot comments ignored", "No code access unless separate tool grant exists"],
|
||||
["Output", "React + post/edit one GFM comment", "No token stream; links replace files/actions"]
|
||||
],
|
||||
"Microsoft Teams": [
|
||||
["Human", "Channel root @Maya · or DM/group message", "Conversation type selects the boundary"],
|
||||
["Ingress", "Verify bot activity + tenant/member", "Persist, scope-check, resolve Paperclip actor"],
|
||||
["Binding", "Channel post thread or active conversation", "Create one PAP issue; explicit New task in DM"],
|
||||
["Turns", "Replies, files, Adaptive Card/task module", "Mention/RSC delivery and current permissions apply"],
|
||||
["Output", "DM native stream; group/channel buffered", "Targeted → DM/text fallback; safe output only"]
|
||||
],
|
||||
Telegram: [
|
||||
["DM", "First message → active issue; /new resets", "New task inline button is equivalent"],
|
||||
["Group", "@maya activates; reply-to-Maya continues", "Privacy-on unrelated traffic is not consumed"],
|
||||
["Forum", "message_thread_id ↔ one PAP issue", "Create/manage topics only with explicit admin grant"],
|
||||
["Ingress", "Verify secret/poll claim; dedupe update_id", "Check chat/user scope; persist; typing/reaction"],
|
||||
["Output", "Throttled post/edit + inline callbacks", "Opaque callback IDs; reply/DM + link fallback"]
|
||||
]
|
||||
};
|
||||
|
||||
function setupDesktop(screen) {
|
||||
const d = setupData[screen.provider];
|
||||
const checkRows = d.verify.map((row, index) => status(532 + (index % 2) * 338, 642 + Math.floor(index / 2) * 28, row[0], row[1])).join("\n");
|
||||
return baseSvg(1280, 800, `${globalSidebar()}${topbar(`CONNECTORS › Connect ${screen.provider}`)}${setupContext(screen.provider)}${heading(screen, "Provider handoff")}
|
||||
${rc(504, 172, 720, 72, 'fill="#e6e6e6"')}${circle(536, 208, 18, 'fill="#fff"')}${tx(568, 202, d.bot, 14, "#000", 'font-weight="700"')}${tx(568, 226, d.identity, 12, "#666")}
|
||||
${rc(504, 264, 344, 132)}${tx(528, 294, "IN PAPERCLIP", 12, "#666", 'font-weight="600"')}${tx(528, 324, d.delivery, 14, "#000", 'font-weight="700"')}${tx(528, 350, d.deliveryNote, 12, "#666")}${tx(528, 378, "Public endpoint copied · deployment detected", 12, "#666")}
|
||||
${rc(504, 412, 344, 172)}${tx(528, 442, "CREDENTIAL REFERENCES", 12, "#666", 'font-weight="600"')}${textLines(528, 472, d.secrets, 12, "#000", 26)}${tx(528, 558, "Values stay masked after save", 12, "#666")}
|
||||
${rc(872, 264, 352, 320)}${tx(896, 294, "AT THE PROVIDER", 12, "#666", 'font-weight="600"')}${d.steps.map((step, index) => `${circle(912, 332 + index * 46, 12, 'fill="#e6e6e6"')}${tx(912, 336 + index * 46, index + 1, 12, "#000", 'text-anchor="middle"')}${tx(938, 336 + index * 46, step, 12, "#000", 'font-weight="600"')}`).join("\n")}${button(896, 510, 304, "Open provider setup ↗")}
|
||||
${rc(504, 604, 720, 92, 'fill="#e6e6e6"')}${tx(528, 628, "VERIFICATION", 12, "#666", 'font-weight="600"')}${checkRows}
|
||||
${button(504, 720, 136, "Save draft")}${button(964, 720, 260, d.action, true)}
|
||||
${annotations([{x:496,y:164,w:736,h:88},{x:496,y:256,w:360,h:148},{x:864,y:256,w:368,h:336},{x:496,y:404,w:360,h:188},{x:496,y:596,w:736,h:108}])}`);
|
||||
}
|
||||
|
||||
function settingsDesktop(screen) {
|
||||
const d = settingsData[screen.provider];
|
||||
const capRows = d.capabilities.map((line, index) => `${tx(896, 236 + index * 32, line, 12, index === 1 && screen.provider === "Microsoft Teams" ? "#666" : "#000")}${tx(1196, 236 + index * 32, index === 1 && screen.provider === "Microsoft Teams" ? "Grant ›" : "", 12, "#666", 'text-anchor="end"')}`).join("\n");
|
||||
return baseSvg(1280, 800, `${globalSidebar()}${topbar(`CONNECTORS › Maya on ${screen.provider} › Settings`)}${detailContext(screen.provider)}${heading(screen)}
|
||||
${rc(504, 168, 344, 168, 'fill="#e6e6e6"')}${tx(528, 198, "REACH", 12, "#666", 'font-weight="600"')}${textLines(528, 228, d.reach, 12, "#000", 25)}${tx(816, 312, "Edit ›", 12, "#000", 'text-anchor="end" font-weight="600"')}
|
||||
${rc(504, 352, 344, 184)}${tx(528, 382, "TASK BOUNDARY", 12, "#666", 'font-weight="600"')}${textLines(528, 414, d.boundary, 12, "#000", 27)}${tx(528, 510, "Default · provider-native and durable", 12, "#666")}
|
||||
${rc(872, 168, 352, 232)}${tx(896, 198, "BEHAVIOR + CAPABILITIES", 12, "#666", 'font-weight="600"')}${capRows}${tx(1196, 378, "Change ›", 12, "#000", 'text-anchor="end" font-weight="600"')}
|
||||
${rc(872, 416, 352, 136)}${tx(896, 446, "SECURITY + DELIVERY", 12, "#666", 'font-weight="600"')}${textLines(896, 474, d.security, 12, "#000", 22)}
|
||||
${rc(504, 568, 720, 80)}${tx(528, 598, "FALLBACK", 12, "#666", 'font-weight="600"')}${tx(528, 626, d.fallback, 12, "#000")}
|
||||
${rc(504, 672, 720, 48)}${tx(528, 702, "Internal reasoning and tool traces are never published.", 12, "#666")}${button(1080, 672, 144, "Save changes", true)}
|
||||
${annotations([{x:496,y:160,w:360,h:184},{x:496,y:344,w:360,h:200},{x:864,y:160,w:368,h:248},{x:864,y:408,w:368,h:152},{x:496,y:560,w:736,h:96}])}`);
|
||||
}
|
||||
|
||||
function interactionsDesktop(screen) {
|
||||
const rows = interactionData[screen.provider];
|
||||
const rendered = rows.map((row, index) => {
|
||||
const y = 218 + index * 98;
|
||||
return `${rc(504, y, 720, 82, index === 2 ? 'fill="#e6e6e6"' : 'fill="#fff"')}${rc(520, y + 17, 104, 48, 'fill="#fff"')}${tx(572, y + 47, row[0], 12, "#000", 'text-anchor="middle" font-weight="700"')}${tx(650, y + 32, row[1], 14, "#000", 'font-weight="600"')}${tx(650, y + 59, row[2], 12, "#666")}${index < rows.length - 1 ? `<path d="M 860 ${y + 82} L 860 ${y + 98}"/><polygon points="860,${y + 98} 854,${y + 89} 866,${y + 89}" fill="#000" stroke="none"/>` : ""}`;
|
||||
}).join("\n");
|
||||
return baseSvg(1280, 800, `${globalSidebar()}${topbar(`CONNECTORS › Maya on ${screen.provider} › Interaction model`)}${detailContext(screen.provider, "Conversations")}${heading(screen)}${tx(504, 188, "NATIVE EVENT", 12, "#666", 'font-weight="600"')}${tx(650, 188, "PROVIDER + PAPERCLIP RESULT", 12, "#666", 'font-weight="600"')}${rendered}${tx(504, 732, "All paths use durable delivery, current authorization, one task binding, and safe outbound projection.", 12, "#666")}${annotations(rows.map((_, index) => ({x:496,y:210+index*98,w:736,h:98})) )}`);
|
||||
}
|
||||
|
||||
function mobileHeader(label) {
|
||||
return `${rc(0, 0, 375, 56)}${tx(16, 35, `‹ ${label}`, 14, "#000", 'font-weight="600"')}${tx(359, 35, "Menu", 12, "#666", 'text-anchor="end"')}`;
|
||||
}
|
||||
|
||||
function mobileTitle(screen, phase) {
|
||||
const shortTitles = {
|
||||
"Connect Maya to GitHub conversations": "Connect Maya to GitHub",
|
||||
"Install Maya in Microsoft Teams": "Install Maya in Teams",
|
||||
"Microsoft Teams settings": "Teams settings",
|
||||
"Microsoft Teams interaction model": "Teams interaction model"
|
||||
};
|
||||
const title = shortTitles[screen.title] ?? screen.title;
|
||||
return `${tx(16, 84, `${screen.provider} · ${phase}`, 12, "#666", 'font-weight="600"')}${tx(16, 116, title, 20, "#000", 'font-weight="700"')}${tx(16, 142, screen.subtitle.length > 55 ? screen.subtitle.slice(0, 54) + "…" : screen.subtitle, 12, "#666")}`;
|
||||
}
|
||||
|
||||
function setupMobile(screen) {
|
||||
const d = setupData[screen.provider];
|
||||
return baseSvg(375, 812, `${mobileHeader("Connectors")}${mobileTitle(screen, "Setup")}
|
||||
${rc(16, 166, 343, 72, 'fill="#e6e6e6"')}${tx(36, 196, d.bot, 14, "#000", 'font-weight="700"')}${tx(36, 220, d.identity, 12, "#666")}
|
||||
${rc(16, 254, 343, 92)}${tx(36, 282, "IN PAPERCLIP", 12, "#666", 'font-weight="600"')}${tx(36, 310, d.delivery, 14, "#000", 'font-weight="700"')}${tx(36, 332, d.deliveryNote.slice(0, 48), 12, "#666")}
|
||||
${rc(16, 362, 343, 188)}${tx(36, 390, "AT THE PROVIDER", 12, "#666", 'font-weight="600"')}${d.steps.map((step,index)=>`${circle(44,420+index*30,9,'fill="#e6e6e6"')}${tx(44,424+index*30,index+1,12,"#000",'text-anchor="middle"')}${tx(64,424+index*30,step.length>40?step.slice(0,39)+"…":step,12,"#000")}`).join("\n")}
|
||||
${rc(16, 566, 343, 72)}${tx(36, 594, "MASKED CREDENTIALS", 12, "#666", 'font-weight="600"')}${tx(36, 620, d.secrets.join(" · ").slice(0, 48), 12, "#000")}
|
||||
${rc(16, 654, 343, 66, 'fill="#e6e6e6"')}${tx(36, 682, "Verification", 12, "#666", 'font-weight="600"')}${tx(36, 706, d.verify.map(row=>`${row[0]} ${row[1]}`).join(" · ").slice(0, 52), 12, "#000")}
|
||||
${button(16, 744, 343, d.action, true)}
|
||||
${annotations([{x:8,y:158,w:359,h:88},{x:8,y:246,w:359,h:108},{x:8,y:354,w:359,h:204},{x:8,y:558,w:359,h:88},{x:8,y:646,w:359,h:82}],true)}`);
|
||||
}
|
||||
|
||||
function settingsMobile(screen) {
|
||||
const d = settingsData[screen.provider];
|
||||
return baseSvg(375, 812, `${mobileHeader(`Maya on ${screen.provider === "Microsoft Teams" ? "Teams" : screen.provider}`)}${mobileTitle(screen, "Settings")}
|
||||
${rc(16, 166, 343, 104, 'fill="#e6e6e6"')}${tx(36, 194, "REACH", 12, "#666", 'font-weight="600"')}${textLines(36, 220, d.reach.slice(0,3), 12, "#000", 20)}
|
||||
${rc(16, 286, 343, 116)}${tx(36, 314, "TASK BOUNDARY", 12, "#666", 'font-weight="600"')}${textLines(36, 340, d.boundary, 12, "#000", 20)}
|
||||
${rc(16, 418, 343, 126)}${tx(36, 446, "BEHAVIOR + CAPABILITIES", 12, "#666", 'font-weight="600"')}${textLines(36, 472, d.capabilities.slice(0,4), 12, "#000", 19)}
|
||||
${rc(16, 560, 343, 104)}${tx(36, 588, "SECURITY + DELIVERY", 12, "#666", 'font-weight="600"')}${textLines(36, 614, d.security.slice(0,3), 12, "#000", 19)}
|
||||
${rc(16, 680, 343, 56)}${tx(36, 704, "FALLBACK", 12, "#666", 'font-weight="600"')}${tx(36, 724, d.fallback.length>49?d.fallback.slice(0,48)+"…":d.fallback, 12, "#000")}
|
||||
${button(16, 752, 343, "Save changes", true)}
|
||||
${annotations([{x:8,y:158,w:359,h:120},{x:8,y:278,w:359,h:132},{x:8,y:410,w:359,h:142},{x:8,y:552,w:359,h:120},{x:8,y:672,w:359,h:72}],true)}`);
|
||||
}
|
||||
|
||||
function interactionsMobile(screen) {
|
||||
const rows = interactionData[screen.provider];
|
||||
const body = rows.map((row,index)=>{const y=166+index*112;return `${rc(16,y,343,96,index===2?'fill="#e6e6e6"':'fill="#fff"')}${rc(32,y+16,64,48,'fill="#fff"')}${tx(64,y+46,row[0],12,"#000",'text-anchor="middle" font-weight="700"')}${tx(112,y+30,row[1].length>37?row[1].slice(0,36)+"…":row[1],12,"#000",'font-weight="600"')}${tx(112,y+54,row[2].length>37?row[2].slice(0,36)+"…":row[2],12,"#666")}${index<4?`<path d="M 188 ${y+96} L 188 ${y+112}"/><polygon points="188,${y+112} 182,${y+103} 194,${y+103}" fill="#000" stroke="none"/>`:""}`;}).join("\n");
|
||||
return baseSvg(375,812,`${mobileHeader(`Maya on ${screen.provider === "Microsoft Teams" ? "Teams" : screen.provider}`)}${mobileTitle(screen,"Interactions")}${body}${tx(16,754,"Durable · authorized · one task · safe output",12,"#666")}${annotations(rows.map((_,index)=>({x:8,y:158+index*112,w:359,h:112})),true)}`);
|
||||
}
|
||||
|
||||
for (const screen of providerScreens) {
|
||||
const desktop = screen.kind === "providerSetup" ? setupDesktop(screen)
|
||||
: screen.kind === "providerSettings" ? settingsDesktop(screen)
|
||||
: interactionsDesktop(screen);
|
||||
const mobile = screen.kind === "providerSetup" ? setupMobile(screen)
|
||||
: screen.kind === "providerSettings" ? settingsMobile(screen)
|
||||
: interactionsMobile(screen);
|
||||
writeFileSync(join(out, `${screen.id}-${screen.slug}.svg`), `${desktop}\n`);
|
||||
writeFileSync(join(out, `${screen.id}-${screen.slug}-mobile.svg`), `${mobile}\n`);
|
||||
}
|
||||
|
||||
console.log(`Generated ${providerScreens.length * 2} provider SVGs`);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,576 @@
|
|||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url));
|
||||
const out = join(root, "wireframes");
|
||||
mkdirSync(out, { recursive: true });
|
||||
mkdirSync(join(root, "screenshots"), { recursive: true });
|
||||
|
||||
const screens = [
|
||||
{
|
||||
id: "01", slug: "connectors-catalog", title: "Connectors", subtitle: "Connect tools and places where people talk to agents.", context: "Apps", active: "Connectors", kind: "catalog",
|
||||
panels: [
|
||||
["Slack", "Tools: 1 account", "Channels: Maya bot · Active", "Add account"],
|
||||
["Microsoft Teams", "Tools: Not connected", "Channels: Available", "Connect"],
|
||||
["Discord", "Tools: Not available", "Channels: Preview", "Connect"],
|
||||
["Telegram", "Tools: Not available", "Channels: 2 bots", "Manage"],
|
||||
["GitHub", "Tools: 1 app", "Channels: Available", "Connect"],
|
||||
],
|
||||
notes: ["Filter by Tools, Channels, or Connected.", "Slack, Teams, Discord, Telegram, and GitHub form the initial supported set.", "Maturity and deployment state control the available action."],
|
||||
},
|
||||
{
|
||||
id: "02", slug: "connection-method", title: "Connect Slack", subtitle: "Choose how Slack and Paperclip should communicate.", context: "Apps", active: "Setup", kind: "choice",
|
||||
panels: [
|
||||
["Agent uses Slack", "Give selected agents Slack tools.", "Agents call Slack during Paperclip runs.", "Uses tool permissions and grants."],
|
||||
["People talk to an agent", "Install one Paperclip agent as a Slack bot.", "Messages become Paperclip task turns.", "Uses channel identity and access rules."],
|
||||
["Separate connections", "These methods do not share credentials.", "Choose the direction before setup.", "Recommended: channel connection"],
|
||||
],
|
||||
notes: ["Two directions are named before credentials are requested.", "The channel method binds one bot to one Paperclip agent.", "Credentials and permissions remain independent."],
|
||||
},
|
||||
{
|
||||
id: "03", slug: "choose-agent-identity", title: "Choose the agent", subtitle: "This Slack bot will always represent one Paperclip agent.", context: "Apps", active: "Setup", kind: "wizard", step: "Step 1 of 7 · Agent & identity",
|
||||
panels: [
|
||||
["Paperclip agent", "Maya · Support lead", "Active · Codex runtime", "Change agent"],
|
||||
["Slack bot preview", "Maya", "@maya-support", "Avatar from agent profile"],
|
||||
["One bot per agent", "Add another Slack app for another agent.", "Native mentions select the agent.", "No hidden dispatcher bot."],
|
||||
],
|
||||
notes: ["Only active, invokable agents can be selected.", "Provider bot identity is previewed beside the Paperclip agent.", "Multiple agents require multiple native bot identities."],
|
||||
},
|
||||
{
|
||||
id: "04", slug: "provider-installation", title: "Install the Slack bot", subtitle: "Bring your own Slack app and verify every connection layer.", context: "Apps", active: "Setup", kind: "wizard", step: "Step 2 of 7 · Provider installation",
|
||||
panels: [
|
||||
["1 · Create the app", "Open generated Slack manifest", "Install or reinstall to workspace", "Invite @maya-support to a channel"],
|
||||
["2 · Save credentials", "Bot token · Secret reference", "Signing secret · Secret reference", "Values are hidden after save"],
|
||||
["3 · Verify", "Bot identity · Passed", "Webhook signature · Passed", "Scopes · 1 action needed"],
|
||||
],
|
||||
notes: ["BYO app setup is the required release path.", "Secrets are stored as Paperclip secret references.", "Credential, signature, scope, and reachability checks are separate."],
|
||||
},
|
||||
{
|
||||
id: "05", slug: "conversation-reach", title: "Choose where Maya listens", subtitle: "Allow exact resources and make activation behavior predictable.", context: "Apps", active: "Setup", kind: "wizard", step: "Step 3 of 7 · Conversation reach",
|
||||
panels: [
|
||||
["Allowed channels", "#customer-support · On", "#product-feedback · On", "+ Add exact channel"],
|
||||
["Thread activation", "Mention Maya in the channel root", "Bot opens thread + one issue", "Continue in thread without mentions"],
|
||||
["Direct messages", "One task per Slack DM thread", "Proactive DMs: Off", "Linked users and guests allowed"],
|
||||
],
|
||||
notes: ["Resource ids, not display names, enforce reach.", "Root mention → native thread → one Paperclip issue is the thread-capable default.", "GitHub binds an existing thread; Telegram uses its stable chat or topic."],
|
||||
},
|
||||
{
|
||||
id: "06", slug: "people-permissions", title: "Choose who people act as", subtitle: "Every external message receives a bounded Paperclip identity.", context: "Apps", active: "Setup", kind: "wizard", step: "Step 4 of 7 · People & permissions",
|
||||
panels: [
|
||||
["Endpoint sponsor", "Dana · Company admin", "Provides a maximum authority envelope", "Change sponsor"],
|
||||
["Linked people", "Act as their Paperclip user", "Current permissions checked each action", "Invite identity link"],
|
||||
["Unlinked people", "Sponsored restricted guest", "May message this task and attach files", "Cannot govern, approve, hire, or reassign"],
|
||||
],
|
||||
notes: ["The endpoint sponsor is visible before activation.", "Linked users are reauthorized with current permissions.", "Guest authority is an intersection and excludes governance."],
|
||||
},
|
||||
{
|
||||
id: "07", slug: "output-interactions", title: "Choose channel behavior", subtitle: "Expose useful progress without exposing Paperclip internals.", context: "Apps", active: "Setup", kind: "wizard", step: "Step 5 of 7 · Output & interactions",
|
||||
panels: [
|
||||
["Acknowledgement & progress", "React with eyes when supported", "Safe milestones: On", "Update every 4 seconds at most"],
|
||||
["Rich output", "Final text, approved files, cards", "Buttons, dropdowns, modals: On", "Unsupported: text + Paperclip link"],
|
||||
["Overlapping messages", "Queue messages on this task", "Other modes: Burst · Debounce · Drop", "Concurrent mode requires explicit selection"],
|
||||
],
|
||||
notes: ["Milestones never include reasoning or raw tool traces.", "Every rich feature has a named text/link fallback.", "Queue is the default concurrency policy."],
|
||||
},
|
||||
{
|
||||
id: "08", slug: "agent-routes", title: "Agent-to-agent routes", subtitle: "Let bots talk only through explicit directed routes.", context: "Apps", active: "Setup", kind: "wizard", step: "Step 6 of 7 · Agent routes",
|
||||
panels: [
|
||||
["Agent routing", "Off by default", "Bot messages are ignored", "Enable with a directed route"],
|
||||
["Allowed route", "Maya in #support → Quinn in #engineering", "Trigger: Native mention only", "Maximum hops: 2"],
|
||||
["Loop protection", "Suppress self and revisited endpoints", "Suppress repeated causal fingerprint", "Keep immutable route audit"],
|
||||
],
|
||||
notes: ["A master default-off control prevents accidental bot loops.", "Routes are directed and resource-scoped.", "Hop, revisit, self, and fingerprint guards are mandatory."],
|
||||
},
|
||||
{
|
||||
id: "09", slug: "review-activate", title: "Review and activate", subtitle: "Verify the bot, its authority, and a real Slack message.", context: "Apps", active: "Setup", kind: "wizard", step: "Step 7 of 7 · Review & activate",
|
||||
panels: [
|
||||
["Configuration", "Maya · @maya-support", "2 allowed channels · DMs on", "Sponsor: Dana · Guest profile: Restricted"],
|
||||
["Required checks", "Credentials · Passed", "Webhook & signature · Passed", "Bot invited to #customer-support · Passed"],
|
||||
["Live test", "1. Mention Maya in the channel root", "2. Bot opens thread + one issue", "3. Follow up there without a mention"],
|
||||
],
|
||||
notes: ["Review summarizes identity, reach, permissions, and behavior.", "The live test proves activation and subscription behavior.", "BYO completion enables activation; managed install is optional."],
|
||||
},
|
||||
{
|
||||
id: "10", slug: "endpoint-overview", title: "Maya on Slack", subtitle: "See what is connected, whether it works, and what needs attention.", context: "Apps", active: "Overview", kind: "detail",
|
||||
panels: [
|
||||
["Endpoint", "Agent: Maya · Support lead", "Bot: @maya-support", "Workspace: Acme"],
|
||||
["Health", "Provider credentials · Healthy", "Direct ingress · Healthy", "Last delivery · 2 minutes ago"],
|
||||
["Activity", "18 conversations · 7 active tasks", "24 linked people · 3 guests", "1 failed publication"],
|
||||
],
|
||||
notes: ["Agent, bot, installation, and endpoint status stay together.", "Health separates credentials, ingress/relay, and delivery.", "Lifecycle controls sit near status; removal remains a danger action."],
|
||||
},
|
||||
{
|
||||
id: "11", slug: "endpoint-access", title: "Access", subtitle: "Manage where the bot listens and who external people represent.", context: "Apps", active: "Access", kind: "table",
|
||||
panels: [
|
||||
["Resources", "#customer-support · Active", "#product-feedback · Active", "#private-escalations · Disabled"],
|
||||
["People", "Ari S. → Ari Stone · Linked", "Jules P. → Sponsored guest", "build-bot → External bot · Routed"],
|
||||
["Policy", "Sponsor: Dana", "Guest: Message + safe files", "Governance: Linked authorized users only"],
|
||||
],
|
||||
notes: ["Resource status and exact provider identity remain visible.", "People rows distinguish linked users, guests, and bots.", "Revoke preserves historical attribution while stopping future authority."],
|
||||
},
|
||||
{
|
||||
id: "12", slug: "endpoint-behavior", title: "Behavior", subtitle: "Edit inbound, outbound, and interaction policies with fallbacks visible.", context: "Apps", active: "Behavior", kind: "detail",
|
||||
panels: [
|
||||
["Inbound", "Root mention → bot thread", "One issue per endpoint thread", "Existing thread / chat fallback shown"],
|
||||
["Outbound", "Acknowledge: Reaction → Ephemeral", "Safe milestones + final output", "Stream: Native → Post and edit"],
|
||||
["Capabilities", "Files · Supported", "Cards/actions/modals · Supported", "Deletes · Append tombstone"],
|
||||
],
|
||||
notes: ["Inbound settings name their task/run consequence.", "Outbound settings show provider fallback order.", "Saving creates a versioned policy with a change preview."],
|
||||
},
|
||||
{
|
||||
id: "13", slug: "conversations-tasks", title: "Conversations", subtitle: "Every bot-owned external thread maps to one Paperclip issue.", context: "Apps", active: "Conversations", kind: "table",
|
||||
panels: [
|
||||
["#customer-support · Refund workflow", "PAP-1842 · In progress", "4 participants · 8m ago", "Subscribed"],
|
||||
["DM with Ari Stone", "PAP-1839 · Waiting for input", "Linked user · 24m ago", "Subscribed"],
|
||||
["#product-feedback · Import CSV", "PAP-1804 · Done", "Detached yesterday", "Open history"],
|
||||
],
|
||||
notes: ["Rows pair one external thread with exactly one endpoint-owned Paperclip issue.", "Filters cover active, waiting, failed, detached, and DMs.", "Detach preserves history and unlocks assignment."],
|
||||
},
|
||||
{
|
||||
id: "14", slug: "deliveries-diagnostics", title: "Activity and deliveries", subtitle: "Diagnose accepted, ignored, retried, and failed external events.", context: "Apps", active: "Activity", kind: "table",
|
||||
panels: [
|
||||
["Inbound mention", "Applied · PAP-1842", "event Ev04…91 · deduped once", "122 ms"],
|
||||
["Outbound final", "Retrying · Slack rate limit", "publication Pb18…40 · attempt 2", "Retry in 28 seconds"],
|
||||
["Button action", "Denied · User not linked", "action Ac77…10 · acknowledged", "Open redacted details"],
|
||||
],
|
||||
notes: ["One ledger covers inbound, outbound, and interactive actions.", "Rows expose dedupe, attempt, timing, and task without payload secrets.", "Replay is idempotent and limited to eligible failures."],
|
||||
},
|
||||
{
|
||||
id: "15", slug: "agent-channels", title: "Maya · Channels", subtitle: "Every place this Paperclip agent can be reached.", context: "Agent", active: "Channels", kind: "agent",
|
||||
panels: [
|
||||
["Slack · @maya-support", "Acme · 2 allowed channels", "Healthy · Root mention opens thread", "7 active tasks"],
|
||||
["Telegram · @maya_helper_bot", "Support group + DMs", "Needs attention · Token expires", "3 active tasks"],
|
||||
["Recent channel tasks", "PAP-1842 · Refund workflow", "PAP-1839 · Ari DM", "PAP-1827 · Product question"],
|
||||
],
|
||||
notes: ["Channels sits under Runtime in agent navigation.", "Endpoint cards retain platform identity, reach, health, and trigger policy.", "Add channel starts Apps with this agent preselected."],
|
||||
},
|
||||
{
|
||||
id: "16", slug: "bound-task", title: "Refund workflow is failing", subtitle: "PAP-1842 · Externally bound to Maya on Slack.", context: "Task", active: "Task", kind: "task",
|
||||
panels: [
|
||||
["Slack · #customer-support", "Thread: Refund workflow", "Assigned agent locked to Maya", "Open Slack · Manage connection"],
|
||||
["Ari S. · External participant", "The refund step is timing out again.", "Linked as Ari Stone", "8 minutes ago"],
|
||||
["Maya · Agent output", "I found the failing retry boundary…", "Publication: Delivered to Slack", "Artifact: retry-analysis.md"],
|
||||
],
|
||||
notes: ["A source banner explains the binding and assignment lock.", "External attribution never impersonates a Paperclip user.", "The composer defaults internal; Send to channel is explicit and previewed."],
|
||||
},
|
||||
{
|
||||
id: "17", slug: "identity-link", title: "Link your Slack identity", subtitle: "Confirm who you will act as when messaging Maya.", context: "Identity", active: "Link", kind: "link",
|
||||
panels: [
|
||||
["Slack identity", "Ari S. · Acme workspace", "Requested by @maya-support", "Expires in 9 minutes"],
|
||||
["Paperclip identity", "Ari Stone · ari@acme.example", "Company: Acme", "Signed in"],
|
||||
["After linking", "Future actions use current permissions", "This does not share Slack credentials", "You can revoke from endpoint Access"],
|
||||
],
|
||||
notes: ["Both identities and company are visible before confirmation.", "Authentication returns to the same single-use intent.", "Expired, used, revoked, and mismatch states fail safely."],
|
||||
},
|
||||
{
|
||||
id: "18", slug: "self-hosted-relay", title: "Ingress for this instance", subtitle: "Use direct HTTPS or an outbound relay for a private Paperclip.", context: "Apps", active: "Overview", kind: "relay",
|
||||
panels: [
|
||||
["Direct HTTPS", "Recommended when Paperclip is public", "Provider sends to this instance", "Current: Not reachable"],
|
||||
["Outbound relay", "Private instance opens one connection", "Encrypted bounded delivery envelopes", "Current: Connected"],
|
||||
["Relay health", "Owner: chat-adapters-dev", "Heartbeat: 12 seconds ago", "Backlog: 0 · Key rotated 8d ago"],
|
||||
],
|
||||
notes: ["Mode comparison starts with detected reachability.", "Enrollment reveals a one-time secret only once.", "Health distinguishes relay receipt from Paperclip processing."],
|
||||
},
|
||||
{
|
||||
id: "19", slug: "adapter-state-matrix", title: "Adapter and state matrix", subtitle: "One UI system covers provider shapes and operational fallbacks.", context: "Apps", active: "Reference", kind: "matrix",
|
||||
panels: [
|
||||
["Workspace apps", "Slack · Teams · Discord · Google Chat", "App registration + tenant + webhook", "Rich interactions and streaming vary"],
|
||||
["Comments and messaging", "GitHub · Linear · Notion · Telegram", "Token/app + resource allowlist", "Thread and mention rules vary"],
|
||||
["Phone, social, and email", "WhatsApp · Twilio · X · Resend · iMessage", "Sender identity + webhook", "Media, window, and rate limits vary"],
|
||||
],
|
||||
notes: ["Provider taxonomy drives setup fields without cloning the wizard.", "Capability rows name supported, fallback, and unavailable behavior.", "Shared states cover loading, empty, degraded, denied, rate-limited, revoked, and dead letter."],
|
||||
},
|
||||
];
|
||||
|
||||
const uiSurfaceSpec = readFileSync(join(root, "2026-09-03-chat-adapters-ui-surfaces.md"), "utf8");
|
||||
const annotationMap = new Map(
|
||||
[...uiSurfaceSpec.matchAll(/### (\d{2})[^\n]*\n\nPurpose:[^\n]*\n\n((?:\d+\.[^\n]*\n){5})/g)].map((match) => [
|
||||
match[1],
|
||||
match[2].trim().split("\n").map((line) => line.replace(/^\d+\.\s*/, "")),
|
||||
]),
|
||||
);
|
||||
|
||||
for (const screen of screens) {
|
||||
screen.annotations = annotationMap.get(screen.id);
|
||||
if (!screen.annotations || screen.annotations.length !== 5) {
|
||||
throw new Error(`Expected five documented annotations for screen ${screen.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const esc = (value) => String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """);
|
||||
|
||||
const text = (x, y, value, size = 14, fill = "#000", extra = "") =>
|
||||
`<text x="${x}" y="${y}" font-size="${size}" stroke="none" fill="${fill}" ${extra}>${esc(value)}</text>`;
|
||||
|
||||
const multiline = (x, y, lines, size = 14, fill = "#666", gap = 24) =>
|
||||
lines.map((line, index) => text(x, y + index * gap, line, size, fill)).join("\n");
|
||||
|
||||
function wrapWords(value, maxCharacters = 48) {
|
||||
const lines = [];
|
||||
let current = "";
|
||||
for (const word of value.split(" ")) {
|
||||
const candidate = current ? `${current} ${word}` : word;
|
||||
if (candidate.length > maxCharacters && current) {
|
||||
lines.push(current);
|
||||
current = word;
|
||||
} else {
|
||||
current = candidate;
|
||||
}
|
||||
}
|
||||
if (current) lines.push(current);
|
||||
return lines.slice(0, 2);
|
||||
}
|
||||
|
||||
const mobileSubtitle = (y, value) => multiline(16, y, wrapWords(value), 12, "#666", 16);
|
||||
|
||||
function desktopSidebar(screen) {
|
||||
const appItems = screen.context === "Agent"
|
||||
? ["Overview", "Instructions", "Skills", "Runtime", "Secrets", "Tools", "Channels", "Permissions"]
|
||||
: screen.context === "Task"
|
||||
? ["Inbox", "Tasks", "Projects", "Agents", "Apps", "Activity"]
|
||||
: ["Connectors", "Review", "Setup", "Overview", "Access", "Behavior", "Conversations", "Activity"];
|
||||
return `
|
||||
<g data-region="navigation">
|
||||
<rect x="0" y="0" width="240" height="800" />
|
||||
${text(24, 40, "Paperclip", 20, "#000", 'font-weight="600"')}
|
||||
${text(24, 72, screen.context, 12, "#666", 'font-weight="600"')}
|
||||
${appItems.map((item, i) => {
|
||||
const y = 96 + i * 48;
|
||||
const active = item === screen.active;
|
||||
return `${active ? `<rect x="8" y="${y - 24}" width="224" height="40" rx="4" fill="#e6e6e6" />` : ""}${text(24, y, item, 14, active ? "#000" : "#666", active ? 'font-weight="600"' : "")}`;
|
||||
}).join("\n")}
|
||||
${text(24, 760, "Acme Company", 14, "#000", 'font-weight="600"')}
|
||||
${text(24, 784, "Operator", 12, "#666")}
|
||||
</g>
|
||||
<g data-region="topbar">
|
||||
<line x1="240" y1="64" x2="1280" y2="64" />
|
||||
${text(264, 40, `${screen.context} / ${screen.title}`, 14, "#666")}
|
||||
<circle cx="1240" cy="32" r="16" fill="#e6e6e6" />
|
||||
</g>`;
|
||||
}
|
||||
|
||||
function annotations(regions, mobile = false) {
|
||||
return `<g data-region="annotations">${regions.map((r, index) => {
|
||||
const n = index + 1;
|
||||
const cx = r.x;
|
||||
const cy = r.y;
|
||||
return `<rect x="${r.x}" y="${r.y}" width="${r.w}" height="${r.h}" rx="4" fill="none" stroke="#d33" stroke-dasharray="6 3" />
|
||||
<circle cx="${cx}" cy="${cy}" r="${mobile ? 8 : 12}" fill="#fff" stroke="#d33" stroke-dasharray="4 2" />
|
||||
${text(cx, cy + (mobile ? 4 : 4), n, 12, "#d33", 'font-weight="700" text-anchor="middle"')}`;
|
||||
}).join("\n")}</g>`;
|
||||
}
|
||||
|
||||
function desktopCard(x, y, width, height, panel, index) {
|
||||
const [heading, ...lines] = panel;
|
||||
return `<g transform="translate(${x},${y})" data-region="panel-${index + 1}">
|
||||
<rect width="${width}" height="${height}" rx="8" />
|
||||
${text(24, 40, heading, 20, "#000", 'font-weight="600"')}
|
||||
${lines.map((line, i) => {
|
||||
const yy = 80 + i * 40;
|
||||
return `<line x1="24" y1="${yy - 16}" x2="${width - 24}" y2="${yy - 16}" stroke="#e6e6e6" />${text(24, yy + 4, line, 14, i === lines.length - 1 ? "#000" : "#666", i === lines.length - 1 ? 'font-weight="600"' : "")}`;
|
||||
}).join("\n")}
|
||||
</g>`;
|
||||
}
|
||||
|
||||
function desktopGeneric(screen) {
|
||||
const contentX = 280;
|
||||
const width = 952;
|
||||
const cards = screen.panels.map((panel, i) => desktopCard(contentX + (i % 3) * 312, 224, 288, 288, panel, i)).join("\n");
|
||||
const step = screen.step ? text(contentX, 96, screen.step, 12, "#666", 'font-weight="600"') : "";
|
||||
const actions = screen.id === "10"
|
||||
? `<g transform="translate(952,104)"><rect width="120" height="40" rx="4" />${text(60, 25, "Test", 14, "#000", 'text-anchor="middle"')}</g><g transform="translate(1088,104)"><rect width="120" height="40" rx="4" fill="#000" />${text(60, 25, "Pause", 14, "#fff", 'font-weight="600" text-anchor="middle"')}</g>`
|
||||
: screen.id === "09"
|
||||
? `<g transform="translate(1040,104)"><rect width="168" height="40" rx="4" fill="#000" />${text(84, 25, "Activate channel", 14, "#fff", 'font-weight="600" text-anchor="middle"')}</g>`
|
||||
: screen.kind === "wizard"
|
||||
? `<g transform="translate(1088,680)"><rect width="120" height="40" rx="4" fill="#000" />${text(60, 25, "Continue", 14, "#fff", 'font-weight="600" text-anchor="middle"')}</g><g transform="translate(952,680)"><rect width="120" height="40" rx="4" />${text(60, 25, "Back", 14, "#000", 'text-anchor="middle"')}</g>`
|
||||
: `<g transform="translate(1088,104)"><rect width="120" height="40" rx="4" fill="#000" />${text(60, 25, screen.id === "15" ? "Add channel" : "Save", 14, "#fff", 'font-weight="600" text-anchor="middle"')}</g>`;
|
||||
const lower = screen.kind === "table"
|
||||
? `<g transform="translate(${contentX},544)"><rect width="928" height="136" rx="8" fill="#e6e6e6" />${text(24, 32, "Selected details", 14, "#000", 'font-weight="600"')}${multiline(24, 64, ["Exact provider and Paperclip identifiers", "Current state, last event, and safe operator actions", "Sensitive payload values remain redacted"], 12, "#666", 24)}</g>`
|
||||
: `<g transform="translate(${contentX},544)"><rect width="928" height="96" rx="8" fill="#e6e6e6" />${text(24, 32, screen.notes[0], 14, "#000", 'font-weight="600"')}${text(24, 64, screen.notes[1], 12, "#666")}</g>`;
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="800" viewBox="0 0 1280 800" font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5">
|
||||
<!-- ${screen.id} · ${esc(screen.title)} · Desktop 1280×800 -->
|
||||
<rect x="0" y="0" width="1280" height="800" />
|
||||
${desktopSidebar(screen)}
|
||||
${step}
|
||||
${text(contentX, 136, screen.title, 28, "#000", 'font-weight="700"')}
|
||||
${text(contentX, 168, screen.subtitle, 14, "#666")}
|
||||
${actions}
|
||||
${screen.kind === "wizard" ? `<line x1="${contentX}" y1="192" x2="1208" y2="192" /><line x1="${contentX}" y1="192" x2="${contentX + Number(screen.id) * 72}" y2="192" />` : ""}
|
||||
${cards}
|
||||
${lower}
|
||||
${annotations([
|
||||
{x: 272, y: 88, w: 944, h: 112},
|
||||
{x: 272, y: 216, w: 304, h: 304},
|
||||
{x: 584, y: 216, w: 304, h: 304},
|
||||
{x: 896, y: 216, w: 320, h: 304},
|
||||
{x: 272, y: 536, w: 944, h: screen.kind === "table" ? 152 : 112},
|
||||
])}
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function desktopCatalog(screen) {
|
||||
const rows = screen.panels.map((panel, i) => {
|
||||
const y = 264 + i * 96;
|
||||
return `<g transform="translate(280,${y})"><rect width="928" height="80" rx="8" ${i === 0 ? 'fill="#e6e6e6"' : ""}/><rect x="16" y="16" width="48" height="48" rx="8" fill="#e6e6e6" />${text(80, 32, panel[0], 14, "#000", 'font-weight="600"')}${text(80, 56, `${panel[1]} · ${panel[2]}`, 12, "#666")}${text(888, 48, panel[3], 14, "#000", 'font-weight="600" text-anchor="end"')}</g>`;
|
||||
}).join("\n");
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="800" viewBox="0 0 1280 800" font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5"><rect x="0" y="0" width="1280" height="800"/>${desktopSidebar(screen)}${text(280,128,screen.title,28,"#000",'font-weight="700"')}${text(280,160,screen.subtitle,14,"#666")}<g transform="translate(280,184)"><rect width="480" height="40" rx="20"/><circle cx="24" cy="20" r="8"/><line x1="32" y1="28" x2="40" y2="36"/>${text(48,25,"Search connectors",14,"#666")}</g><g transform="translate(784,184)"><rect width="424" height="40" rx="4"/>${text(16,25,"All Tools Channels Connected",14,"#000")}</g>${rows}${annotations([{x:8,y:64,w:232,h:408},{x:272,y:176,w:944,h:56},{x:272,y:256,w:944,h:472},{x:272,y:448,w:944,h:80},{x:1072,y:256,w:144,h:472}])}</svg>`;
|
||||
}
|
||||
|
||||
function desktopTask(screen) {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="800" viewBox="0 0 1280 800" font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5"><rect width="1280" height="800"/>${desktopSidebar(screen)}${text(280,112,screen.title,28,"#000",'font-weight="700"')}${text(280,144,screen.subtitle,14,"#666")}<g transform="translate(280,176)"><rect width="928" height="88" rx="8" fill="#e6e6e6"/>${text(24,32,screen.panels[0][0],14,"#000",'font-weight="600"')}${text(24,56,screen.panels[0][1],12,"#666")}${text(24,76,screen.panels[0][2],12,"#666")}${text(888,48,"Open Slack",14,"#000",'font-weight="600" text-anchor="end"')}</g><g transform="translate(280,296)"><rect width="640" height="128" rx="8"/><circle cx="32" cy="32" r="16" fill="#e6e6e6"/>${text(64,32,screen.panels[1][0],14,"#000",'font-weight="600"')}${text(64,64,screen.panels[1][1],14,"#000")}${text(64,96,`${screen.panels[1][2]} · ${screen.panels[1][3]}`,12,"#666")}</g><g transform="translate(280,448)"><rect width="640" height="144" rx="8" fill="#e6e6e6"/><circle cx="32" cy="32" r="16" fill="#e6e6e6"/>${text(64,32,screen.panels[2][0],14,"#000",'font-weight="600"')}${text(64,64,screen.panels[2][1],14,"#000")}${text(64,96,screen.panels[2][2],12,"#666")}${text(64,120,screen.panels[2][3],12,"#666")}</g><g transform="translate(944,296)"><rect width="264" height="296" rx="8"/>${text(24,32,"Properties",20,"#000",'font-weight="600"')}${multiline(24,72,["Status · In progress","Assignee · Maya (locked)","Priority · High","Project · Support","Channel · Slack"],14,"#666",40)}<g transform="translate(24,232)"><rect width="216" height="40" rx="4"/>${text(108,25,"Detach channel",14,"#000",'text-anchor="middle"')}</g></g><g transform="translate(280,624)"><rect width="928" height="104" rx="8"/>${text(16,32,"Internal note",12,"#666")}<line x1="16" y1="56" x2="752" y2="56" stroke="#666"/><rect x="760" y="16" width="152" height="40" rx="4" fill="#000"/>${text(836,41,"Add comment",14,"#fff",'font-weight="600" text-anchor="middle"')}${text(16,88,"○ Send to channel · Preview required",12,"#000")}</g>${annotations([{x:272,y:168,w:944,h:104},{x:272,y:288,w:656,h:144},{x:272,y:440,w:656,h:160},{x:272,y:616,w:944,h:120},{x:936,y:288,w:280,h:312}])}</svg>`;
|
||||
}
|
||||
|
||||
function desktopLink(screen) {
|
||||
const cards = screen.panels.map((p,i)=>desktopCard(280+i*312,248,288,248,p,i)).join("\n");
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="800" viewBox="0 0 1280 800" font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5"><rect width="1280" height="800"/>${text(48,48,"Paperclip",20,"#000",'font-weight="600"')}<circle cx="1232" cy="40" r="16" fill="#e6e6e6"/>${text(640,144,screen.title,28,"#000",'font-weight="700" text-anchor="middle"')}${text(640,176,screen.subtitle,14,"#666",'text-anchor="middle"')}${cards}<g transform="translate(488,544)"><rect width="304" height="48" rx="4" fill="#000"/>${text(152,30,"Confirm identity link",14,"#fff",'font-weight="600" text-anchor="middle"')}</g>${text(640,624,"Single use · Expires in 9 minutes · Revoke from endpoint Access",12,"#666",'text-anchor="middle"')}${annotations([{x:272,y:104,w:936,h:88},{x:272,y:240,w:304,h:264},{x:584,y:240,w:304,h:264},{x:480,y:536,w:320,h:64},{x:376,y:600,w:528,h:40}])}</svg>`;
|
||||
}
|
||||
|
||||
function desktopMatrix(screen) {
|
||||
const rows = [
|
||||
["Workspace app","Slack · Teams · Discord","Yes","Native/edit","Rich"],
|
||||
["Comment system","GitHub · Linear · Notion","Yes","Edit","Link/card"],
|
||||
["Bot token","Telegram","Yes","Draft/edit","Keyboard"],
|
||||
["Meta messaging","WhatsApp · Messenger","DM","Post","Buttons"],
|
||||
["Phone/iMessage","Twilio · Photon · Linq","DM","Post","Limited"],
|
||||
["Social/email","X · Resend","Mixed","Post/edit","Mixed"],
|
||||
];
|
||||
const body = rows.map((r,i)=>`<g transform="translate(280,${272+i*56})"><rect width="928" height="56" ${i%2?'fill="#e6e6e6"':''}/>${text(16,34,r[0],14,"#000",'font-weight="600"')}${text(200,34,r[1],14,"#666")}${text(520,34,r[2],14,"#666")}${text(640,34,r[3],14,"#666")}${text(792,34,r[4],14,"#666")}</g>`).join("\n");
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="800" viewBox="0 0 1280 800" font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5"><rect width="1280" height="800"/>${desktopSidebar(screen)}${text(280,128,screen.title,28,"#000",'font-weight="700"')}${text(280,160,screen.subtitle,14,"#666")}<g transform="translate(280,208)"><rect width="928" height="56" fill="#e6e6e6"/>${text(16,34,"Setup pattern",12,"#000",'font-weight="600"')}${text(200,34,"Providers",12,"#000",'font-weight="600"')}${text(520,34,"Mentions",12,"#000",'font-weight="600"')}${text(640,34,"Streaming",12,"#000",'font-weight="600"')}${text(792,34,"Interactions",12,"#000",'font-weight="600"')}</g>${body}<g transform="translate(280,632)"><rect width="928" height="88" rx="8"/>${text(24,32,"Shared operational states",14,"#000",'font-weight="600"')}${text(24,64,"Loading · Empty · Degraded · Denied · Unsupported fallback · Rate limited · Revoked · Dead letter",12,"#666")}</g>${annotations([{x:272,y:264,w:192,h:352},{x:784,y:200,w:432,h:416},{x:464,y:200,w:320,h:416},{x:272,y:200,w:192,h:64},{x:272,y:624,w:944,h:104}])}</svg>`;
|
||||
}
|
||||
|
||||
function mobileHeader(screen) {
|
||||
return `<rect x="0" y="0" width="375" height="64"/><text x="16" y="40" font-size="14" font-weight="600" stroke="none" fill="#000">${esc(screen.context)}</text><text x="343" y="40" font-size="14" text-anchor="end" stroke="none" fill="#666">Menu</text>`;
|
||||
}
|
||||
|
||||
function mobileCard(y, panel, index, compact = false) {
|
||||
const [heading, ...lines] = panel;
|
||||
const height = compact ? 120 : 144;
|
||||
return `<g transform="translate(16,${y})" data-region="panel-${index + 1}"><rect width="343" height="${height}" rx="8" ${index===0?'fill="#e6e6e6"':''}/>${text(16,32,heading,14,"#000",'font-weight="600"')}${lines.slice(0,3).map((line,i)=>text(16,64+i*24,line,12,i===2?"#000":"#666",i===2?'font-weight="600"':"")).join("\n")}</g>`;
|
||||
}
|
||||
|
||||
function mobileGeneric(screen) {
|
||||
const start = screen.step ? 184 : 160;
|
||||
const compact = screen.panels.length > 3;
|
||||
const gap = compact ? 128 : 152;
|
||||
const cards = screen.panels.slice(0,4).map((p,i)=>mobileCard(start+i*gap,p,i,compact)).join("\n");
|
||||
const lastY = start + Math.min(screen.panels.length,4)*gap;
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="375" height="812" viewBox="0 0 375 812" font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5"><rect width="375" height="812"/>${mobileHeader(screen)}${screen.step?text(16,88,screen.step,12,"#666",'font-weight="600"'):""}${text(16,screen.step?120:104,screen.title,20,"#000",'font-weight="600"')}${mobileSubtitle(screen.step?144:128,screen.subtitle)}${cards}<g transform="translate(16,${Math.min(lastY,744)})"><rect width="343" height="48" rx="4" fill="#000"/>${text(171,30,screen.kind==="wizard"?"Continue":screen.id==="15"?"Add channel":"Save",14,"#fff",'font-weight="600" text-anchor="middle"')}</g>${annotations([{x:8,y:72,w:359,h:88},{x:8,y:start-8,w:359,h:160},{x:8,y:start+gap-8,w:359,h:160},{x:8,y:start+gap*2-8,w:359,h:160},{x:8,y:Math.min(lastY-8,736),w:359,h:64}],true)}</svg>`;
|
||||
}
|
||||
|
||||
function mobileCatalog(screen) {
|
||||
const cards=screen.panels.map((panel,index)=>`<g transform="translate(16,${232+index*104})" data-region="provider-${index+1}"><rect width="343" height="96" rx="8" ${index===0?'fill="#e6e6e6"':''}/>${text(16,24,panel[0],14,"#000",'font-weight="600"')}${text(16,48,panel[1],12,"#666")}${text(16,68,panel[2],12,"#666")}${text(327,88,panel[3],12,"#000",'font-weight="600" text-anchor="end"')}</g>`).join("\n");
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="375" height="812" viewBox="0 0 375 812" font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5"><rect width="375" height="812"/>${mobileHeader(screen)}${text(16,104,screen.title,20,"#000",'font-weight="600"')}${mobileSubtitle(128,screen.subtitle)}<g transform="translate(16,152)"><rect width="343" height="48" rx="24"/><circle cx="24" cy="24" r="8"/><line x1="32" y1="32" x2="40" y2="40"/>${text(48,30,"Search connectors",14,"#666")}</g>${text(16,216,"All Tools Channels Connected",12,"#000",'font-weight="600"')}${cards}${annotations([{x:8,y:0,w:359,h:64},{x:8,y:144,w:359,h:88},{x:8,y:224,w:359,h:528},{x:24,y:488,w:184,h:32},{x:240,y:224,w:128,h:528}],true)}</svg>`;
|
||||
}
|
||||
|
||||
function mobileTask(screen) {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="375" height="812" viewBox="0 0 375 812" font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5"><rect width="375" height="812"/>${mobileHeader(screen)}${text(16,96,"PAP-1842",12,"#666")}${text(16,128,screen.title,20,"#000",'font-weight="600"')}<g transform="translate(16,152)"><rect width="343" height="104" rx="8" fill="#e6e6e6"/>${text(16,32,"Slack · #customer-support",14,"#000",'font-weight="600"')}${text(16,56,"Assigned agent locked to Maya",12,"#666")}${text(16,80,"Open Slack · Manage connection",12,"#000",'font-weight="600"')}</g><g transform="translate(16,280)"><rect width="343" height="120" rx="8"/><circle cx="32" cy="32" r="16" fill="#e6e6e6"/>${text(56,32,"Ari S. · External participant",12,"#000",'font-weight="600"')}${text(16,72,"The refund step is timing out again.",14,"#000")}${text(16,96,"Linked as Ari Stone · 8m ago",12,"#666")}</g><g transform="translate(16,424)"><rect width="343" height="136" rx="8" fill="#e6e6e6"/>${text(16,32,"Maya · Agent output",12,"#000",'font-weight="600"')}${text(16,64,"I found the failing retry boundary…",14,"#000")}${text(16,96,"Delivered to Slack",12,"#666")}${text(16,120,"retry-analysis.md",12,"#000",'font-weight="600"')}</g><g transform="translate(16,584)"><rect width="343" height="136" rx="8"/>${text(16,32,"Internal note",12,"#666")}<line x1="16" y1="56" x2="327" y2="56" stroke="#666"/>${text(16,88,"○ Send to channel · Preview",12,"#000")}<rect x="207" y="80" width="120" height="40" rx="4" fill="#000"/>${text(267,105,"Comment",14,"#fff",'font-weight="600" text-anchor="middle"')}</g>${annotations([{x:8,y:144,w:359,h:120},{x:8,y:272,w:359,h:136},{x:8,y:416,w:359,h:152},{x:8,y:576,w:359,h:152},{x:200,y:648,w:152,h:64}],true)}</svg>`;
|
||||
}
|
||||
|
||||
function mobileLink(screen) {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="375" height="812" viewBox="0 0 375 812" font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5"><rect width="375" height="812"/>${text(16,40,"Paperclip",14,"#000",'font-weight="600"')}${text(16,96,screen.title,20,"#000",'font-weight="600"')}${mobileSubtitle(120,screen.subtitle)}${screen.panels.map((p,i)=>mobileCard(152+i*152,p,i)).join("\n")}<g transform="translate(16,624)"><rect width="343" height="48" rx="4" fill="#000"/>${text(171,30,"Confirm identity link",14,"#fff",'font-weight="600" text-anchor="middle"')}</g>${text(187,704,"Single use · Expires in 9 minutes",12,"#666",'text-anchor="middle"')}${annotations([{x:8,y:72,w:359,h:56},{x:8,y:144,w:359,h:160},{x:8,y:296,w:359,h:160},{x:8,y:616,w:359,h:64},{x:8,y:688,w:359,h:40}],true)}</svg>`;
|
||||
}
|
||||
|
||||
function mobileMatrix(screen) {
|
||||
const rows=[["Workspace apps","Slack · Teams · Discord"],["Comment systems","GitHub · Linear · Notion"],["Bot token","Telegram"],["Meta messaging","WhatsApp · Instagram"],["Phone/iMessage","Twilio · Photon · Linq"],["Social/email","X · Resend"]];
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="375" height="812" viewBox="0 0 375 812" font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5"><rect width="375" height="812"/>${mobileHeader(screen)}${text(16,104,screen.title,20,"#000",'font-weight="600"')}${mobileSubtitle(128,screen.subtitle)}${rows.map((r,i)=>`<g transform="translate(16,${160+i*72})"><rect width="343" height="64" rx="4" ${i%2?'fill="#e6e6e6"':''}/>${text(16,26,r[0],14,"#000",'font-weight="600"')}${text(16,50,r[1],12,"#666")}${text(327,38,"›",20,"#000",'text-anchor="end"')}</g>`).join("\n")}<g transform="translate(16,616)"><rect width="343" height="104" rx="8"/>${text(16,32,"Shared states",14,"#000",'font-weight="600"')}${text(16,56,"Loading · Empty · Degraded · Denied",12,"#666")}${text(16,80,"Rate limited · Revoked · Dead letter",12,"#666")}</g>${annotations([{x:8,y:152,w:176,h:448},{x:184,y:152,w:183,h:448},{x:8,y:152,w:359,h:232},{x:8,y:384,w:359,h:216},{x:8,y:608,w:359,h:120}],true)}</svg>`;
|
||||
}
|
||||
|
||||
function flowSvg() {
|
||||
const cells = screens.map((s, i) => {
|
||||
const col = i % 5;
|
||||
const row = Math.floor(i / 5);
|
||||
const x = 48 + col * 240;
|
||||
const y = 96 + row * 168;
|
||||
return `<g transform="translate(${x},${y})"><rect width="192" height="112" rx="8" ${[8,9,14,15].includes(i)?'fill="#e6e6e6"':''}/><rect x="16" y="16" width="40" height="40" rx="4" fill="#e6e6e6"/>${text(72,32,s.id,12,"#666",'font-weight="600"')}${text(72,56,s.title.length>14?`${s.title.slice(0,14)}…`:s.title,14,"#000",'font-weight="600"')}${text(16,88,i<9?"SETUP":i<14?"MANAGE":"RELATED",12,"#666",'font-weight="600"')}</g>`;
|
||||
}).join("\n");
|
||||
const arrows=[];
|
||||
for(let i=0;i<screens.length-1;i++){
|
||||
const c=i%5,r=Math.floor(i/5); const nc=(i+1)%5,nr=Math.floor((i+1)/5);
|
||||
if(r===nr){const x=240+c*240,y=152+r*168;arrows.push(`<line x1="${x}" y1="${y}" x2="${x+32}" y2="${y}"/><polygon points="${x+32},${y} ${x+24},${y-8} ${x+24},${y+8}" fill="#000" stroke="none"/>`);}
|
||||
}
|
||||
arrows.push(`<path d="M 1008 208 C 1104 232, 1104 248, 48 264" fill="none" stroke="#000" stroke-dasharray="6 3"/>`);
|
||||
arrows.push(`<path d="M 768 544 C 768 640, 1008 640, 1008 600" fill="none" stroke="#000" stroke-dasharray="6 3"/>`);
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="800" viewBox="0 0 1280 800" font-family="-apple-system, system-ui, sans-serif" fill="#fff" stroke="#000" stroke-width="1.5"><rect width="1280" height="800"/>${text(48,40,"Chat adapters · Paperclip product flow",28,"#000",'font-weight="700"')}${text(48,72,"Solid arrows follow the primary review path; dashed arrows mark management, identity, relay, diagnostics, and detach branches.",14,"#666")}${cells}${arrows}${annotations([{x:40,y:88,w:1168,h:312},{x:40,y:416,w:1168,h:312}])}</svg>`;
|
||||
}
|
||||
|
||||
function viewerHtml() {
|
||||
const templatePath = join(root, "../../../packages/skills-catalog/catalog/bundled/product/wireframe/assets/site-template.html");
|
||||
const template = readFileSync(templatePath, "utf8");
|
||||
const style = template.match(/<style>[\s\S]*?<\/style>/)?.[0];
|
||||
if (!style) throw new Error(`Could not read viewer styles from ${templatePath}`);
|
||||
|
||||
const toc = screens.map((screen) =>
|
||||
`<a href="#s${screen.id}"><span class="num">${Number(screen.id)}</span>${esc(screen.title)}</a>`,
|
||||
).join("\n");
|
||||
|
||||
const sections = screens.map((screen) => {
|
||||
const notes = screen.annotations.map((note, index) =>
|
||||
`<li><b>${index + 1}</b> — ${esc(note).replaceAll("**", "")}</li>`,
|
||||
).join("\n");
|
||||
const rationale = screen.notes.map((note) => esc(note)).join(" ");
|
||||
return `<section id="s${screen.id}">
|
||||
<div class="lede">${Number(screen.id) <= 9 ? "Setup" : Number(screen.id) <= 14 ? "Endpoint management" : "Related surfaces"}</div>
|
||||
<h2><span class="step-num">${Number(screen.id)}.</span>${esc(screen.title)}</h2>
|
||||
<p class="desc">${esc(screen.subtitle)}</p>
|
||||
<div class="grid">
|
||||
<div class="wire" data-zoom data-caption="${screen.id} · ${esc(screen.title)} (desktop)">
|
||||
<div class="label"><span>${screen.id}-${screen.slug}.svg</span><span>1280×800 · desktop</span></div>
|
||||
<img src="wireframes/${screen.id}-${screen.slug}.svg" alt="${esc(screen.title)} desktop wireframe" />
|
||||
</div>
|
||||
<div class="wire mobile-wire mobile-col" data-zoom data-caption="${screen.id} · ${esc(screen.title)} (mobile)">
|
||||
<div class="label"><span>mobile</span><span>375×812</span></div>
|
||||
<img src="wireframes/${screen.id}-${screen.slug}-mobile.svg" alt="${esc(screen.title)} mobile wireframe" />
|
||||
</div>
|
||||
<div class="notes-col">
|
||||
<div class="notes">
|
||||
<h3>Annotations</h3>
|
||||
<ul>${notes}</ul>
|
||||
<div class="why"><b>Rationale:</b> ${rationale}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>`;
|
||||
}).join("\n");
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Paperclip chat adapters — planning review</title>
|
||||
${style}
|
||||
<style>
|
||||
.doc-links { display: flex; flex-wrap: wrap; gap: 8px 16px; margin-top: 16px; }
|
||||
.doc-links a { min-height: 48px; display: inline-flex; align-items: center; font-size: 13px; font-weight: 600; }
|
||||
.notice { max-width: var(--maxw); margin: -32px 0 48px; padding: 14px 18px; background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--accent); border-radius: 4px; }
|
||||
.notice p { margin: 0; }
|
||||
code { font-size: 0.92em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<details class="toc">
|
||||
<summary class="toc-summary">
|
||||
<span><span class="crumb">Chat adapters · planning</span><br><span class="title">Jump to a screen</span></span>
|
||||
<span class="chevron" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<nav class="toc-body" aria-label="Section navigation">
|
||||
<h1>Chat adapters</h1>
|
||||
<div style="font-size: 13px; color: var(--muted); margin-bottom: 16px;">Planning review package</div>
|
||||
<h2>Documents</h2>
|
||||
<a href="2026-09-03-chat-adapters-architecture.md"><span class="num">A</span>Architecture</a>
|
||||
<a href="2026-09-03-chat-adapters-research-notes.md"><span class="num">R</span>Research notes</a>
|
||||
<a href="2026-09-03-chat-adapters-ui-surfaces.md"><span class="num">U</span>UI specification</a>
|
||||
<h2>Flow</h2>
|
||||
<a href="#flow"><span class="num">⤳</span>Product flow</a>
|
||||
<h2>Screens</h2>
|
||||
${toc}
|
||||
<h2>Review</h2>
|
||||
<a href="#coverage"><span class="num">✓</span>Coverage and sources</a>
|
||||
</nav>
|
||||
</details>
|
||||
<main>
|
||||
<header class="hero">
|
||||
<div class="crumb">Paperclip · Chat adapters · Planning artifact</div>
|
||||
<h1>Connect one Paperclip agent to every place people already work</h1>
|
||||
<p>This package defines the administration, agent, task, identity-link, and relay surfaces for durable external chat endpoints. Paperclip remains the control plane; provider channels are communication media.</p>
|
||||
<div class="doc-links">
|
||||
<a href="2026-09-03-chat-adapters-architecture.md">Read architecture plan</a>
|
||||
<a href="2026-09-03-chat-adapters-research-notes.md">Read research appendix</a>
|
||||
<a href="2026-09-03-chat-adapters-ui-surfaces.md">Read UI surface specification</a>
|
||||
</div>
|
||||
<div class="pills">
|
||||
<span class="pill">19 product screens</span>
|
||||
<span class="pill">Desktop + mobile</span>
|
||||
<span class="pill">Slack-first · 5-provider launch</span>
|
||||
<span class="pill">Click any wireframe to zoom</span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="notice" role="note"><p><b>Review convention:</b> red dashed marks and numbered circles are annotations only. They are not proposed Paperclip interface elements.</p></div>
|
||||
<section id="flow" class="flow-section">
|
||||
<div class="lede">Navigation and product flow</div>
|
||||
<h2>From Apps discovery to an externally bound task</h2>
|
||||
<p class="desc">Solid arrows follow setup and activation. Dashed paths branch to endpoint management, identity linking, private-instance relay, diagnostics, and detach/rebind. This is a product navigation flow, not a system architecture diagram.</p>
|
||||
<div class="wire" data-zoom data-caption="Chat adapters product flow">
|
||||
<div class="label"><span>flow.svg</span><span>1280×800</span></div>
|
||||
<img src="wireframes/flow.svg" alt="Chat adapters navigation and product flow" />
|
||||
</div>
|
||||
<div class="notes"><h3>Annotations</h3><ul><li><b>1</b> — Discovery, connection-method choice, setup, review, and activation.</li><li><b>2</b> — Endpoint operations and the agent, task, identity-link, relay, and adapter-state branches.</li></ul></div>
|
||||
</section>
|
||||
${sections}
|
||||
<section id="coverage">
|
||||
<div class="lede">Coverage and sources</div>
|
||||
<h2>Review checklist</h2>
|
||||
<div class="notes">
|
||||
<ul>
|
||||
<li><b>Paperclip invariant:</b> agents, tasks, runs, permissions, approvals, budgets, artifacts, and audit history remain authoritative in Paperclip.</li>
|
||||
<li><b>Provider model:</b> one installed native bot identity maps to exactly one Paperclip agent endpoint.</li>
|
||||
<li><b>First supported set:</b> Slack, Microsoft Teams, Discord, Telegram, and GitHub.</li>
|
||||
<li><b>Thread model:</b> a root mention creates/opens a provider thread and one endpoint-owned Paperclip issue where supported; GitHub binds an existing issue/PR/discussion thread; Telegram uses the stable chat/topic boundary.</li>
|
||||
<li><b>Chat SDK coverage:</b> events, streaming, cards, actions, modals, commands, emoji, files, DMs, ephemeral output, and overlap policies appear in screens 07, 12, 14, and 19.</li>
|
||||
<li><b>Research pins:</b> Paperclip <code>b84964e5a2fa8b1e6498a1ccb471f6adba97d470</code>; Vercel Chat SDK <code>51322dde8f4aafd8a7fc7a20cbfd7ae45cafaa5c</code>; OpenTag <code>6a770d862349f8e996c23c145aef6d6275914a23</code>.</li>
|
||||
<li><b>Current-state screenshots:</b> omitted because no deterministic local fixture was used; no reference UI has been invented.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<div class="footer">Generated from Paperclip's bundled <code>wireframe</code> skill viewer template. Wires use black 1.5 strokes, white surfaces, grayscale placeholders, an 8px rhythm, and 12/14/20/28 type sizes. Red is reserved for review annotations.</div>
|
||||
</main>
|
||||
</div>
|
||||
<div class="lightbox" id="lb" aria-hidden="true">
|
||||
<span class="close" id="lbClose" role="button" aria-label="Close preview">×</span>
|
||||
<img id="lbImg" alt="" />
|
||||
<div class="caption" id="lbCap"></div>
|
||||
</div>
|
||||
<script>
|
||||
const lb = document.getElementById('lb');
|
||||
const lbImg = document.getElementById('lbImg');
|
||||
const lbCap = document.getElementById('lbCap');
|
||||
document.querySelectorAll('[data-zoom]').forEach((el) => {
|
||||
el.addEventListener('click', () => {
|
||||
const target = el.tagName === 'IMG' ? el : el.querySelector('img');
|
||||
if (!target) return;
|
||||
lbImg.src = target.src;
|
||||
lbImg.alt = target.alt;
|
||||
lbCap.textContent = el.dataset.caption || target.alt || '';
|
||||
lb.classList.add('open');
|
||||
lb.setAttribute('aria-hidden', 'false');
|
||||
});
|
||||
});
|
||||
function closeLightbox() { lb.classList.remove('open'); lb.setAttribute('aria-hidden', 'true'); }
|
||||
lb.addEventListener('click', closeLightbox);
|
||||
document.getElementById('lbClose').addEventListener('click', closeLightbox);
|
||||
document.addEventListener('keydown', (event) => { if (event.key === 'Escape') closeLightbox(); });
|
||||
const tocElement = document.querySelector('details.toc');
|
||||
const media = window.matchMedia('(max-width: 900px)');
|
||||
const setToc = () => { tocElement.open = !media.matches; };
|
||||
setToc();
|
||||
media.addEventListener('change', setToc);
|
||||
tocElement.querySelectorAll('.toc-body a').forEach((link) => link.addEventListener('click', () => { if (media.matches) tocElement.open = false; }));
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
for (const screen of screens) {
|
||||
const desktop = screen.kind === "catalog" ? desktopCatalog(screen)
|
||||
: screen.kind === "task" ? desktopTask(screen)
|
||||
: screen.kind === "link" ? desktopLink(screen)
|
||||
: screen.kind === "matrix" ? desktopMatrix(screen)
|
||||
: desktopGeneric(screen);
|
||||
const mobile = screen.kind === "catalog" ? mobileCatalog(screen)
|
||||
: screen.kind === "task" ? mobileTask(screen)
|
||||
: screen.kind === "link" ? mobileLink(screen)
|
||||
: screen.kind === "matrix" ? mobileMatrix(screen)
|
||||
: mobileGeneric(screen);
|
||||
writeFileSync(join(out, `${screen.id}-${screen.slug}.svg`), `${desktop}\n`);
|
||||
writeFileSync(join(out, `${screen.id}-${screen.slug}-mobile.svg`), `${mobile}\n`);
|
||||
}
|
||||
|
||||
writeFileSync(join(out, "flow.svg"), `${flowSvg()}\n`);
|
||||
writeFileSync(join(root, "index.html"), `${viewerHtml()}\n`);
|
||||
console.log(`Generated ${screens.length * 2 + 1} SVGs and index.html in ${root}`);
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
export const permissionModel = [
|
||||
[
|
||||
"Provider availability",
|
||||
"Slack, Teams, and Telegram decide where the bot is installed or invited. GitHub decides which repositories belong to the App installation."
|
||||
],
|
||||
[
|
||||
"Paperclip enablement",
|
||||
"Paperclip responds only in provider resources that a Paperclip administrator has enabled for this connection. Invitation or installation alone is not permission to create a task."
|
||||
],
|
||||
[
|
||||
"Effective reach",
|
||||
"A message is eligible only when the provider delivers it, its resource is enabled in Paperclip, the connection is active, and the sender has authority for the requested action."
|
||||
],
|
||||
[
|
||||
"Safe default",
|
||||
"The destination used for the successful setup test becomes the first enabled resource. Resources discovered later start disabled."
|
||||
]
|
||||
];
|
||||
|
||||
export const providerManagement = {
|
||||
Slack: {
|
||||
id: "14",
|
||||
slug: "slack-settings",
|
||||
short: "Slack",
|
||||
providerAction: "Add Maya to another Slack channel ↗",
|
||||
providerActionHelp: "Opens Slack instructions. After Maya is invited, the channel appears here disabled.",
|
||||
settingsTitle: "Slack settings",
|
||||
settingsSubtitle: "Enable the Slack channels where Maya may create and continue tasks.",
|
||||
resourcesTitle: "Channels",
|
||||
resourcesIntro: "Only channels where Maya is already a member can be enabled.",
|
||||
resources: [
|
||||
["#customer-support · Acme", "Invited in Slack · Enabled", true],
|
||||
["#incidents · Acme", "Invited in Slack · Enabled", true],
|
||||
["#product · Acme", "Invited in Slack · Not enabled", false]
|
||||
],
|
||||
conversationToggles: [
|
||||
["Allow direct messages", "People may start private tasks by messaging Maya.", true]
|
||||
],
|
||||
accessTitle: "Slack access",
|
||||
accessSubtitle: "Decide how people are identified when they message Maya.",
|
||||
unlinkedLabel: "Allow unlinked people",
|
||||
unlinkedDetail: "In enabled channels, unlinked Slack members can start and continue tasks with restricted access.",
|
||||
identityHint: "Slack workspace ID + user ID",
|
||||
linked: [
|
||||
["Ari Chen · U0184", "ari@acme.com · Member", "Revoke"],
|
||||
["Sam Rivera · U0191", "sam@acme.com · Viewer", "Revoke"]
|
||||
],
|
||||
conversationsTitle: "Slack conversations",
|
||||
conversationsSubtitle: "Conversations created through this connection.",
|
||||
openProvider: "Open Slack",
|
||||
conversations: [
|
||||
["#customer-support · Refund timeout", "PAP-1842 · Refund workflow is failing", "Working · 18s"],
|
||||
["#incidents · Queue delay", "PAP-1838 · Investigate queue delay", "Waiting · 12m"],
|
||||
["Direct message · Ari Chen", "PAP-1831 · Customer export", "Completed · 2h"]
|
||||
]
|
||||
},
|
||||
GitHub: {
|
||||
id: "17",
|
||||
slug: "github-settings",
|
||||
short: "GitHub",
|
||||
providerAction: "Manage GitHub installation ↗",
|
||||
providerActionHelp: "Opens GitHub. Repositories added to the App installation appear here disabled.",
|
||||
settingsTitle: "GitHub settings",
|
||||
settingsSubtitle: "Enable the repositories where Maya may respond to mentions.",
|
||||
resourcesTitle: "Repositories",
|
||||
resourcesIntro: "Only repositories selected in the GitHub App installation can be enabled.",
|
||||
resources: [
|
||||
["acme/api", "Available in GitHub installation · Enabled", true],
|
||||
["acme/web", "Available in GitHub installation · Enabled", true],
|
||||
["acme/docs", "Available in GitHub installation · Not enabled", false]
|
||||
],
|
||||
conversationToggles: [],
|
||||
accessTitle: "GitHub access",
|
||||
accessSubtitle: "Decide how people are identified when they mention Maya.",
|
||||
unlinkedLabel: "Allow unlinked people",
|
||||
unlinkedDetail: "In enabled repositories, unlinked GitHub users can start and continue tasks with restricted access.",
|
||||
identityHint: "GitHub host + numeric user ID",
|
||||
linked: [
|
||||
["arichen · 481902", "ari@acme.com · Member", "Revoke"],
|
||||
["sam-rivera · 592113", "sam@acme.com · Viewer", "Revoke"]
|
||||
],
|
||||
conversationsTitle: "GitHub conversations",
|
||||
conversationsSubtitle: "Conversations created through this connection.",
|
||||
openProvider: "Open GitHub",
|
||||
conversations: [
|
||||
["acme/api · Issue #482", "PAP-1850 · Retry API timeouts", "Working · 3m"],
|
||||
["acme/web · Pull request #912", "PAP-1846 · Review checkout change", "Waiting · 22m"],
|
||||
["acme/api · Review thread", "PAP-1829 · Fix response typing", "Completed · 1d"]
|
||||
]
|
||||
},
|
||||
"Microsoft Teams": {
|
||||
id: "20",
|
||||
slug: "teams-settings",
|
||||
short: "Teams",
|
||||
providerAction: "Add Maya to another team ↗",
|
||||
providerActionHelp: "Opens Teams instructions. Channels in the newly installed team appear here disabled.",
|
||||
settingsTitle: "Microsoft Teams settings",
|
||||
settingsSubtitle: "Enable the Teams channels where Maya may create and continue tasks.",
|
||||
resourcesTitle: "Channels",
|
||||
resourcesIntro: "Only channels in teams where Maya is installed can be enabled.",
|
||||
resources: [
|
||||
["Support / General · Acme", "Installed in Teams · Enabled", true],
|
||||
["Engineering / Incidents · Acme", "Installed in Teams · Enabled", true],
|
||||
["Product / General · Acme", "Installed in Teams · Not enabled", false]
|
||||
],
|
||||
conversationToggles: [
|
||||
["Allow direct messages", "People may start tasks in personal chats with Maya.", true],
|
||||
["Allow group chats", "People may add Maya to a group chat and start tasks there.", false]
|
||||
],
|
||||
accessTitle: "Microsoft Teams access",
|
||||
accessSubtitle: "Decide how people are identified when they message Maya.",
|
||||
unlinkedLabel: "Allow unlinked people",
|
||||
unlinkedDetail: "In enabled Teams conversations, unlinked members can start and continue tasks with restricted access.",
|
||||
identityHint: "Microsoft tenant ID + Entra object ID",
|
||||
linked: [
|
||||
["Ari Chen · 62af…91c", "ari@acme.com · Member", "Revoke"],
|
||||
["Sam Rivera · 74bd…10a", "sam@acme.com · Viewer", "Revoke"]
|
||||
],
|
||||
conversationsTitle: "Microsoft Teams conversations",
|
||||
conversationsSubtitle: "Conversations created through this connection.",
|
||||
openProvider: "Open Teams",
|
||||
conversations: [
|
||||
["Support / General · Refund timeout", "PAP-1861 · Fix refund timeout", "Working · 42s"],
|
||||
["Engineering / Incidents · Queue delay", "PAP-1857 · Diagnose queue delay", "Waiting · 8m"],
|
||||
["Personal chat · Ari Chen", "PAP-1841 · Export account history", "Completed · 4h"]
|
||||
]
|
||||
},
|
||||
Telegram: {
|
||||
id: "23",
|
||||
slug: "telegram-settings",
|
||||
short: "Telegram",
|
||||
providerAction: "Add Maya to another Telegram chat ↗",
|
||||
providerActionHelp: "Opens instructions. After Maya receives a message there, the chat appears here disabled.",
|
||||
settingsTitle: "Telegram settings",
|
||||
settingsSubtitle: "Enable the Telegram chats and topics where Maya may create and continue tasks.",
|
||||
resourcesTitle: "Chats and topics",
|
||||
resourcesIntro: "Only chats where the bot is present and discovered can be enabled.",
|
||||
resources: [
|
||||
["Operations group", "Bot is present · Enabled", true],
|
||||
["Support forum / Refunds", "Bot is present · Enabled", true],
|
||||
["Product group", "Bot is present · Not enabled", false]
|
||||
],
|
||||
conversationToggles: [
|
||||
["Allow direct messages", "People may start private tasks by messaging Maya.", true]
|
||||
],
|
||||
accessTitle: "Telegram access",
|
||||
accessSubtitle: "Decide how people are identified when they message Maya.",
|
||||
unlinkedLabel: "Allow unlinked people",
|
||||
unlinkedDetail: "In enabled chats, unlinked Telegram users can start and continue tasks with restricted access.",
|
||||
identityHint: "Telegram bot ID + numeric user ID",
|
||||
linked: [
|
||||
["Ari Chen · 18409211", "ari@acme.com · Member", "Revoke"],
|
||||
["Sam Rivera · 18410482", "sam@acme.com · Viewer", "Revoke"]
|
||||
],
|
||||
conversationsTitle: "Telegram conversations",
|
||||
conversationsSubtitle: "Conversations created through this connection.",
|
||||
openProvider: "Open Telegram",
|
||||
conversations: [
|
||||
["Operations group · Deployment alert", "PAP-1870 · Check deployment alert", "Working · 25s"],
|
||||
["Support forum / Refunds", "PAP-1866 · Trace missing refund", "Waiting · 6m"],
|
||||
["Private chat · Ari Chen", "PAP-1852 · Prepare customer export", "Completed · 3h"]
|
||||
]
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { providerScreens as v2Screens } from "./platform-wireframe-data.mjs";
|
||||
|
||||
const interactionAnnotations = {
|
||||
"15": [
|
||||
"Ari starts in a Slack channel with a root @maya mention; unrelated root messages do not start work.",
|
||||
"Maya acknowledges inside a Slack thread, making the thread—not the channel—the visible conversation boundary.",
|
||||
"Paperclip creates exactly one assigned issue and shows its Slack source, external participant, and publication state.",
|
||||
"Ari continues by replying in the same thread without another mention; files and actions remain in that context.",
|
||||
"Maya's safe progress and final answer publish in the thread; failures offer retry or a Paperclip link."
|
||||
],
|
||||
"18": [
|
||||
"Ari mentions the bot in an existing GitHub issue, PR conversation, or inline review thread.",
|
||||
"Maya acknowledges with a reaction and one GitHub-Flavored Markdown comment rather than opening another thread.",
|
||||
"Paperclip binds that exact GitHub object or review thread to one assigned issue; PR conversation and inline review stay distinct.",
|
||||
"Later comments continue the same issue, while bot-authored comments and duplicate deliveries are ignored.",
|
||||
"Progress edits the existing comment; files and governed actions use authenticated Paperclip links."
|
||||
],
|
||||
"21": [
|
||||
"Ari mentions Maya in a new Teams channel post; that post and its replies are the native thread.",
|
||||
"Maya acknowledges under the post. If the installed permissions cannot deliver unmentioned replies, the bot says to mention Maya again.",
|
||||
"Paperclip creates one assigned issue and records tenant, team/channel, thread, and external participant attribution.",
|
||||
"Replies, files, and Adaptive Card or task-module actions continue only when current Teams delivery and Paperclip permissions allow.",
|
||||
"DMs may stream natively; channel and group output buffers or edits, with targeted-message, DM, or text-link fallback."
|
||||
],
|
||||
"24": [
|
||||
"In a DM, Ari's first message creates the active issue; New task or /new deliberately starts another.",
|
||||
"In a privacy-on group, @maya starts work and replying to Maya continues; unrelated group traffic is not consumed.",
|
||||
"A forum topic can bind one issue through message_thread_id when the bot is present and allowed.",
|
||||
"Paperclip shows the active issue and makes the linear-chat boundary explicit instead of implying a Slack-style native thread.",
|
||||
"Maya uses throttled post/edit and inline buttons; unsupported or governed actions return text or DM with a Paperclip link."
|
||||
]
|
||||
};
|
||||
|
||||
const interactionTitles = {
|
||||
"15": "How Slack conversations work",
|
||||
"18": "How GitHub conversations work",
|
||||
"21": "How Microsoft Teams conversations work",
|
||||
"24": "How Telegram conversations work"
|
||||
};
|
||||
|
||||
const interactionSubtitles = {
|
||||
"15": "What Ari sees in Slack and the single Paperclip issue created behind the thread.",
|
||||
"18": "What Ari sees in GitHub and how the existing object becomes one Paperclip issue.",
|
||||
"21": "What Ari sees in a channel thread, with separate DM and group-chat behavior.",
|
||||
"24": "How DMs, privacy-on groups, and forum topics establish an explicit active issue."
|
||||
};
|
||||
|
||||
export const providerScreens = v2Screens.map((screen) => {
|
||||
if (screen.kind !== "providerInteractions") return { ...screen };
|
||||
return {
|
||||
...screen,
|
||||
title: interactionTitles[screen.id],
|
||||
subtitle: interactionSubtitles[screen.id],
|
||||
annotations: interactionAnnotations[screen.id],
|
||||
rationale: "This is a product-behavior walkthrough: the external conversation people see beside the Paperclip issue it creates."
|
||||
};
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue