Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "22"
Expand Down Expand Up @@ -53,7 +53,7 @@ jobs:

# pnpm via corepack — the version is pinned by the `packageManager` field
# in package.json, so every contributor and CI run use the same pnpm.
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
Expand All @@ -70,14 +70,47 @@ jobs:
- name: Unit tests
run: pnpm test

pi-peers:
name: pi adapter (SDK ${{ matrix.pi }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
pi: ["0.80.3", "0.85.1"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "22"
cache: "pnpm"
- name: Install locked dependencies
run: pnpm install --frozen-lockfile --registry https://registry.npmjs.org/
# The regular Node 22/24 suite uses the locked 0.85.1 adapter dev peers.
# Also exercise the former adapter resolution, rather than accidentally
# claiming the root SDK version is used by every workspace package.
- name: Install previous adapter peers
if: matrix.pi == '0.80.3'
run: >-
pnpm --filter pi-taskflow add --save-dev --save-exact
@earendil-works/pi-agent-core@0.80.3
@earendil-works/pi-ai@0.80.3
@earendil-works/pi-coding-agent@0.80.3
@earendil-works/pi-tui@0.80.3
--registry https://registry.npmjs.org/
- name: Verify resolved SDK and run complete adapter suite
run: pnpm run test:pi:peers
env:
PI_TEST_PEER_VERSION: ${{ matrix.pi }}

e2e:
# Required status context: keep this literal stable; the job now covers all MCP hosts.
name: e2e (codex MCP, network-free)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
Expand Down Expand Up @@ -133,7 +166,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
Expand All @@ -155,7 +188,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
Expand Down Expand Up @@ -187,7 +220,7 @@ jobs:
# The docs source records last-modified timestamps from git history.
fetch-depth: 0

- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
Expand All @@ -213,7 +246,7 @@ jobs:
security-events: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
- uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
languages: javascript-typescript
- uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
- uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
4 changes: 2 additions & 2 deletions .github/workflows/deploy-website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
# in the root package.json, so CI uses the same pnpm as local dev. The
# website is a pnpm workspace member (pnpm-workspace.yaml), so one
# `pnpm install` at the root installs every package including the website.
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0

- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
Expand Down Expand Up @@ -76,4 +76,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
22 changes: 21 additions & 1 deletion .github/workflows/plugin-scanner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,29 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: hashgraph-online/ai-plugin-scanner-action@55616c962cf86368423f7673b2ecdfdbe613d1af # v1
- uses: hashgraph-online/ai-plugin-scanner-action@484da6f8e99a057233b939ab50450a14a68f5a1c # v1.2.624
with:
plugin_dir: "."
mode: scan
# v1.2.624 makes Cisco opt-in; retain the previous deep-scan coverage.
install_cisco: "true"
cisco_skill_scan: "on"
cisco_policy: balanced
online: "false"
submission_enabled: "false"
pr_comment: "off"
format: json
output: plugin-security-report.json
min_score: 80
fail_on_severity: high
# Always print the full finding payload, even when the gate fails. Keep
# detailed evidence in this job rather than suppressing or baselining it.
- name: Show complete scanner findings
if: always()
run: |
if [ -f plugin-security-report.json ]; then
cat plugin-security-report.json
fi
- name: Require completed Cisco deep scanning
if: always()
run: node scripts/verify-plugin-scan-coverage.mjs plugin-security-report.json
2 changes: 1 addition & 1 deletion .github/workflows/publish-charterarc-experimental.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
echo "PUBLISH_SHA=$TAG_COMMIT" >> "$GITHUB_ENV"
echo "Verified $EXPECT at $TAG_COMMIT is reachable from origin/main"

- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "22"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
exit 1
fi
echo "Verified $GITHUB_REF_NAME at $TAG_COMMIT is reachable from origin/main"
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "22"
Expand Down
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,20 @@
"test:e2e-grok-mcp": "node --conditions=development --experimental-strip-types packages/grok-taskflow/test/e2e-grok-mcp.mts",
"test:e2e-pi-terminal-reap": "node --conditions=development --experimental-strip-types packages/pi-taskflow/test/e2e-terminal-reap.mts",
"test:hermes": "PI_TASKFLOW_BUILTIN_AGENTS_DIR= node --conditions=development --experimental-strip-types --test 'packages/hermes-taskflow/test/*.test.ts'",
"test:e2e-hermes-mcp": "node --conditions=development --experimental-strip-types packages/hermes-taskflow/test/e2e-hermes-mcp.mts"
"test:e2e-hermes-mcp": "node --conditions=development --experimental-strip-types packages/hermes-taskflow/test/e2e-hermes-mcp.mts",
"test:pi:peers": "node scripts/verify-pi-peers.mjs && pnpm run test:pi"
},
"devDependencies": {
"@earendil-works/pi-agent-core": "^0.84.2",
"@earendil-works/pi-ai": "^0.84.2",
"@earendil-works/pi-coding-agent": "^0.84.2",
"@earendil-works/pi-tui": "^0.84.2",
"@earendil-works/pi-agent-core": "^0.85.1",
"@earendil-works/pi-ai": "^0.85.1",
"@earendil-works/pi-coding-agent": "^0.85.1",
"@earendil-works/pi-tui": "^0.85.1",
"@types/node": "^26",
"charterarc": "workspace:*",
"taskflow-control": "workspace:*",
"taskflow-core": "workspace:*",
"taskflow-hosts": "workspace:*",
"typebox": "^1.3.14",
"typebox": "^1.3.30",
"typescript": "^7.0.2"
}
}
7 changes: 7 additions & 0 deletions packages/pi-taskflow/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,12 @@
"@earendil-works/pi-coding-agent": "*",
"@earendil-works/pi-tui": "*",
"typebox": "*"
},
"devDependencies": {
"@earendil-works/pi-agent-core": "0.85.1",
"@earendil-works/pi-ai": "0.85.1",
"@earendil-works/pi-coding-agent": "0.85.1",
"@earendil-works/pi-tui": "0.85.1",
"typebox": "1.3.30"
}
}
21 changes: 21 additions & 0 deletions packages/taskflow-hosts/test/plugin-scan-coverage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { verifyPluginScanCoverage } from "../../../scripts/verify-plugin-scan-coverage.mjs";

const enabled = { name: "taskflow / cisco-skill-scanner", status: "enabled", metadata: { policy: "balanced" } };
const report = (integrations: unknown[]) => ({ schema_version: "scan-result.v1", summary: { integrations } });

test("plugin scan coverage accepts completed deep scans and ignores unrelated MCP status", () => {
assert.equal(verifyPluginScanCoverage(report([enabled, { name: "cisco-mcp-scanner", status: "skipped" }])), 1);
});

test("plugin scan coverage fails closed for missing, skipped, failed, or timed-out scans", () => {
for (const status of ["unavailable", "skipped", "failed", "timed_out", undefined]) {
assert.throws(() => verifyPluginScanCoverage(report([{ ...enabled, status }])), /deep scan must complete/);
}
for (const invalid of [{}, { schema_version: "scan-result.v1" }, report([]), report([{ name: "cisco-mcp-scanner", status: "enabled" }])]) {
assert.throws(() => verifyPluginScanCoverage(invalid));
}
assert.throws(() => verifyPluginScanCoverage(report([enabled, { ...enabled, status: "failed" }])));
assert.throws(() => verifyPluginScanCoverage(report([{ ...enabled, metadata: { policy: "permissive" } }])));
});
71 changes: 53 additions & 18 deletions packages/taskflow-hosts/test/publish-verification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ test("publish workflow pins actions and isolates npm provenance from release per
const source = readFileSync(new URL("../../../.github/workflows/publish.yml", import.meta.url), "utf8");
const uses = source.match(/^\s*- uses: .+$/gm) ?? [];
assert.equal(uses.length, 4);
for (const use of uses) assert.match(use, /@[0-9a-f]{40} # v\d+$/);
for (const use of uses) assert.match(use, /@[0-9a-f]{40} # v\d+(?:\.\d+\.\d+)?$/);
assert.equal(
uses.filter((use) => use.includes("actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7")).length,
2,
);
assert.ok(uses.some((use) => use.includes("pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6")));
assert.ok(uses.some((use) => use.includes("pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0")));
assert.ok(uses.some((use) => use.includes("actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7")));
assert.doesNotMatch(source, /uses:\s+\S+@v\d+/);

Expand Down Expand Up @@ -128,33 +128,68 @@ test("publish workflow smokes, publishes, and verifies one deterministic tarball
assert.doesNotMatch(workflow, /pnpm publish --filter/);
});

// Enumerate uses directives before parsing their refs/comments. A restrictive
// match must never silently skip an action when Dependabot changes its comment.
function verifyActionPins(source: string, trustedPins: ReadonlyMap<string, string>, file: string): number {
let count = 0;
for (const line of source.split("\n")) {
if (!/^\s*(?:-\s+)?uses:/.test(line)) continue;
const match = line.match(/^\s*(?:-\s+)?uses:\s+([^@\s]+)@([0-9a-f]{40})\s+#\s+(v\d+(?:\.\d+\.\d+)?)\s*$/);
assert.ok(match, `${file}: action must use a full commit SHA and a major or semver comment: ${line.trim()}`);
const [, action, revision] = match;
assert.equal(trustedPins.get(action!), revision, `${file}: ${action} is not pinned to its verified tag commit`);
count++;
}
return count;
}

test("action pin validation accepts major and semver comments without skipping either", () => {
const sha = "ea17c68df8912ef543352723c149a84f56e3d413";
const pins = new Map([["pnpm/action-setup", sha]]);
assert.equal(verifyActionPins(` - uses: pnpm/action-setup@${sha} # v6\n uses: pnpm/action-setup@${sha} # v6.1.0`, pins, "fixture"), 2);
for (const use of [
`pnpm/action-setup@${"0".repeat(40)} # v6.1.0`,
`unknown/action@${sha} # v6.1.0`,
"pnpm/action-setup@v6.1.0 # v6.1.0",
`pnpm/action-setup@${sha}`,
`pnpm/action-setup@${sha} # v6.1`,
`"pnpm/action-setup@${sha}" # v6.1.0`,
"",
]) {
assert.throws(() => verifyActionPins(` - uses: ${use}`, pins, "fixture"), `must reject: ${use}`);
}
});

test("every repository workflow pins third-party actions to verified full SHAs", () => {
const workflowDir = new URL("../../../.github/workflows/", import.meta.url);
const trustedPins = new Map([
["actions/checkout", "3d3c42e5aac5ba805825da76410c181273ba90b1"],
["pnpm/action-setup", "0977fd99725f1db4007ccb2928dbb4e90d06cc86"],
["pnpm/action-setup", "ea17c68df8912ef543352723c149a84f56e3d413"], // v6.1.0
["actions/setup-node", "820762786026740c76f36085b0efc47a31fe5020"],
["actions/upload-pages-artifact", "fc324d3547104276b827a68afc52ff2a11cc49c9"],
["actions/deploy-pages", "cd2ce8fcbc39b97be8ca5fce6e763baed58fa128"],
["github/codeql-action/init", "ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd"],
["github/codeql-action/analyze", "ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd"],
["hashgraph-online/ai-plugin-scanner-action", "55616c962cf86368423f7673b2ecdfdbe613d1af"],
["actions/deploy-pages", "368f82528645a54fb793d4d04e342629a3f51346"],
["github/codeql-action/init", "cdf488f595d80d6e07e03d4674febd5ab45fa938"], // v4.37.9
["github/codeql-action/analyze", "cdf488f595d80d6e07e03d4674febd5ab45fa938"],
["hashgraph-online/ai-plugin-scanner-action", "484da6f8e99a057233b939ab50450a14a68f5a1c"],
]);
const files = readdirSync(workflowDir).filter((file) => /\.ya?ml$/.test(file));
assert.ok(files.length > 0, "no workflow files found");
let actionCount = 0;
for (const file of files) {
const source = readFileSync(new URL(file, workflowDir), "utf8");
for (const line of source.split("\n")) {
const match = line.match(/^\s*(?:-\s+)?uses:\s+([^@\s]+)@([^\s]+)(?:\s+#\s+(v\d+))?\s*$/);
if (!match) continue;
actionCount++;
const [, action, revision, versionComment] = match;
assert.match(revision ?? "", /^[0-9a-f]{40}$/, `${file}: action must use a full commit SHA: ${line.trim()}`);
assert.match(versionComment ?? "", /^v\d+$/, `${file}: pinned action must retain a major-version comment`);
assert.equal(trustedPins.get(action ?? ""), revision, `${file}: ${action} is not pinned to its verified tag commit`);
}
assert.doesNotMatch(source, /^\s*(?:-\s+)?uses:\s+[^\s]+@(?![0-9a-f]{40}\b)/m, `${file}: mutable action ref`);
actionCount += verifyActionPins(readFileSync(new URL(file, workflowDir), "utf8"), trustedPins, file);
}
assert.ok(actionCount > 0, "no third-party actions found");
});

test("scanner upgrades retain the high-severity and minimum-score gates", () => {
const workflow = readFileSync(new URL("../../../.github/workflows/plugin-scanner.yml", import.meta.url), "utf8");
assert.match(workflow, /min_score: 80/);
assert.match(workflow, /fail_on_severity: high/);
assert.match(workflow, /install_cisco: "true"/);
assert.match(workflow, /cisco_skill_scan: "on"/);
assert.match(workflow, /cisco_policy: balanced/);
assert.match(workflow, /if: always\(\)\n run: node scripts\/verify-plugin-scan-coverage\.mjs plugin-security-report\.json/);
assert.match(workflow, /online: "false"/);
assert.match(workflow, /submission_enabled: "false"/);
assert.doesNotMatch(workflow, /continue-on-error:\s*true/);
});
Loading
Loading