Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
bec3bf6
feat: modularize skills artifacts and providers
bashybaranaba Aug 20, 2026
5fa7790
Merge pull request #297 from Arttribute/codex/modular-skills-artifacts
bashybaranaba Aug 20, 2026
3bc303e
ci: surface AWS CodeBuild deployment failures
bashybaranaba Aug 20, 2026
70b81e5
Merge pull request #299 from Arttribute/codex/aws-deploy-diagnostics
bashybaranaba Aug 20, 2026
58b917c
fix: use portable UUID default for UI plugins
bashybaranaba Aug 20, 2026
e5140cf
Merge pull request #300 from Arttribute/codex/fix-ui-plugin-migration
bashybaranaba Aug 20, 2026
0cb9d60
fix: make generated UI plugins reliably embeddable
bashybaranaba Aug 20, 2026
44b8eca
fix: allow plugin modules in opaque sandboxes
bashybaranaba Aug 20, 2026
c9ba155
fix: preserve plugin origin for browser storage
bashybaranaba Aug 21, 2026
2cdb531
feat: harden native Commons UI plugins
bashybaranaba Aug 23, 2026
ed81fff
feat: make Commons widgets live and native
bashybaranaba Aug 23, 2026
9f218f8
Rebuild the landing page as the studio shell (#312)
bashybaranaba Aug 23, 2026
3883e13
Rework the landing carousel into a horizontal card (#314)
bashybaranaba Aug 23, 2026
5fc9035
Move the composer below the carousel and the CTA into the card (#316)
bashybaranaba Aug 23, 2026
e51adbf
Size the landing page to the studio composer (#318)
bashybaranaba Aug 23, 2026
a1b13a8
Clean up skills UI and unify scheduled tasks iconography (#320)
bashybaranaba Aug 23, 2026
b5e1923
feat: make agent runs provenance-first
bashybaranaba Aug 26, 2026
e6d6c7c
chore: capture migration failures in AWS diagnostics
bashybaranaba Aug 26, 2026
290521d
fix: target failed API image in migration probe
bashybaranaba Aug 26, 2026
15127fd
fix: run migration probe from source
bashybaranaba Aug 26, 2026
fc76b03
fix: qualify provenance event UUID function
bashybaranaba Aug 26, 2026
94e81bb
feat: make provenance first across agents and workflows
bashybaranaba Aug 26, 2026
6c117cb
ci: prevent deploys after failed image builds
bashybaranaba Aug 26, 2026
57ceee3
perf: record reusable embedding compatibility metadata
bashybaranaba Aug 26, 2026
473c4a5
test: lock embedding reuse provenance contract
bashybaranaba Aug 26, 2026
082aace
feat: add authenticated human approval provenance UX
bashybaranaba Aug 26, 2026
959c22c
build: gate staging on tests and clean runtime scan
bashybaranaba Aug 26, 2026
e748d24
fix: run migrations with explicit container entrypoint
bashybaranaba Aug 26, 2026
a9e58c7
ci: align staging verification with Node 22
bashybaranaba Aug 26, 2026
b16f86a
ci: bound ECR scan gate payload
bashybaranaba Aug 26, 2026
1e22790
build: pin current verified Chrome runtime
bashybaranaba Aug 26, 2026
b548cbb
fix: allow durable human approval workflow state
bashybaranaba Aug 27, 2026
da89757
fix(api): preserve workflow identity after approval
bashybaranaba Aug 27, 2026
01cfdc8
fix(provenance): namespace runtime entity roles
bashybaranaba Aug 28, 2026
5775f70
test(provenance): handle optional bundle entities
bashybaranaba Aug 28, 2026
0014345
test(provenance): model supplemental service actor
bashybaranaba Aug 28, 2026
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
5 changes: 5 additions & 0 deletions .changeset/modular-skills-artifacts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@agent-commons/sdk": minor
---

Add per-agent skill assignments, agent-scoped library items, configurable capability providers, portable skill imports, and sandboxed UI plugin management.
24 changes: 23 additions & 1 deletion .github/workflows/deploy-commons-api-aws.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,29 @@ jobs:
--query 'builds[0].buildStatus' --output text)
case "$status" in
SUCCEEDED) exit 0 ;;
FAILED|FAULT|STOPPED|TIMED_OUT) exit 1 ;;
FAILED|FAULT|STOPPED|TIMED_OUT)
echo "CodeBuild finished with status: $status"
aws codebuild batch-get-builds \
--ids "${{ steps.build.outputs.id }}" \
--query 'builds[0].phases[?phaseStatus==`FAILED`].{phase:phaseType,contexts:contexts}' \
--output json || true

log_group=$(aws codebuild batch-get-builds \
--ids "${{ steps.build.outputs.id }}" \
--query 'builds[0].logs.groupName' --output text)
log_stream=$(aws codebuild batch-get-builds \
--ids "${{ steps.build.outputs.id }}" \
--query 'builds[0].logs.streamName' --output text)
if [ -n "$log_group" ] && [ "$log_group" != "None" ] && \
[ -n "$log_stream" ] && [ "$log_stream" != "None" ]; then
aws logs get-log-events \
--log-group-name "$log_group" \
--log-stream-name "$log_stream" \
--limit 300 \
--query 'events[].message' --output text || true
fi
exit 1
;;
*) sleep 15 ;;
esac
done
27 changes: 24 additions & 3 deletions .github/workflows/diagnose-codebuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,11 @@ jobs:
--max-results 20 \
--query '{serviceDeployments:serviceDeployments[*].{arn:serviceDeploymentArn,status:status,statusReason:statusReason,createdAt:createdAt,startedAt:startedAt,finishedAt:finishedAt,targetRevision:targetServiceRevisionArn}}' \
--output json)
available_secret_keys=$(aws secretsmanager get-secret-value \
available_secret_json=$(aws secretsmanager get-secret-value \
--secret-id "$RUNTIME_SECRET_ARN" \
--query SecretString \
--output text | jq -c 'keys | sort')
--output text)
available_secret_keys=$(jq -c 'keys | sort' <<<"$available_secret_json")
required_secret_keys=$(sed \
-e '/^[[:space:]]*#/d' \
-e '/^[[:space:]]*$/d' \
Expand All @@ -143,13 +144,33 @@ jobs:
--argjson required "$required_secret_keys" \
--argjson available "$available_secret_keys" \
'$required - $available')

# Re-run the pending migrations from the uploaded source and
# capture the real error through the diagnostic
# S3 channel. The GitHub deploy role intentionally cannot read
# CodeBuild's CloudWatch stream.
corepack enable pnpm
corepack prepare pnpm@9.15.3 --activate
pnpm install --frozen-lockfile --filter commons-api...
set +e
POSTGRES_HOST="$(jq -r .POSTGRES_HOST <<<"$available_secret_json")" \
POSTGRES_PORT="$(jq -r .POSTGRES_PORT <<<"$available_secret_json")" \
POSTGRES_DATABASE="$(jq -r .POSTGRES_DATABASE <<<"$available_secret_json")" \
POSTGRES_USER="$(jq -r .POSTGRES_USER <<<"$available_secret_json")" \
POSTGRES_PASSWORD="$(jq -r .POSTGRES_PASSWORD <<<"$available_secret_json")" \
POSTGRES_SSL="$(jq -r '.POSTGRES_SSL // "disable"' <<<"$available_secret_json")" \
pnpm --filter commons-api migrate > /tmp/migration-probe.log 2>&1
migration_exit=$?
set -e
jq -n \
--argjson stack "$stack_json" \
--argjson service "$service_json" \
--argjson deploymentList "$deployment_list" \
--argjson availableSecretKeys "$available_secret_keys" \
--argjson missingSecretKeys "$missing_secret_keys" \
'{stack: $stack, service: $service, deploymentList: $deploymentList, runtimeSecret: {availableKeys: $availableSecretKeys, missingKeys: $missingSecretKeys}}' \
--argjson migrationExit "$migration_exit" \
--rawfile migrationLog /tmp/migration-probe.log \
'{stack: $stack, service: $service, deploymentList: $deploymentList, runtimeSecret: {availableKeys: $availableSecretKeys, missingKeys: $missingSecretKeys}, migrationProbe: {exitCode: $migrationExit, log: $migrationLog}}' \
> /tmp/service-deployment.json
curl --fail-with-body --silent --show-error -X PUT -H 'Content-Type:' --data-binary @/tmp/service-deployment.json "$DIAGNOSTIC_PUT_URL"
YAML
Expand Down
70 changes: 65 additions & 5 deletions apps/commons-api-gateway/scripts/smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,48 @@ delete process.env.AGENT_COMMONS_INTERNAL_URL;
delete process.env.COMMON_OS_INTERNAL_URL;
export {};

const { createGatewayApp } = await import("../src/index.js");
const { createGatewayApp, publicAssetRequestHeaders } = await import(
"../src/index.js"
);
const app = createGatewayApp();

const failures: string[] = [];

const sanitizedPublicHeaders = publicAssetRequestHeaders({
authorization: "Bearer should-not-leave-the-gateway",
cookie: "session=should-not-leave-the-gateway",
"proxy-authorization": "Basic should-not-leave-the-gateway",
"x-owner-id": "owner-1",
"x-initiator": "user-1",
"x-commons-actor-id": "actor-1",
"x-commons-signature": "forged",
accept: "text/html",
});
for (const sensitiveHeader of [
"authorization",
"cookie",
"proxy-authorization",
"x-owner-id",
"x-initiator",
"x-commons-actor-id",
"x-commons-signature",
]) {
if (sanitizedPublicHeaders.has(sensitiveHeader)) {
failures.push(`public asset proxy leaked ${sensitiveHeader}`);
}
}
if (sanitizedPublicHeaders.get("accept") !== "text/html") {
failures.push("public asset proxy removed a harmless content header");
}

async function expectStatus(
label: string,
request: Promise<Response> | Response,
expected: number,
) {
const response = await request;
if (response.status !== expected) {
failures.push(
`${label}: expected ${expected}, got ${response.status}`,
);
failures.push(`${label}: expected ${expected}, got ${response.status}`);
}
}

Expand All @@ -43,10 +70,19 @@ const publicRoutes: Array<[string, RequestInit?]> = [
["/v1/oauth/providers/google"],
["/v1/oauth/callback/google"],
["/v1/billing/webhook", { method: "POST" }],
["/v1/previews/example-project/"],
["/v1/previews/example-project/assets/index.js"],
[
"/v1/ui-plugin-host?entry=https%3A%2F%2Fapi.agentcommons.io%2Fv1%2Fpreviews%2Fexample-project%2Fdeployments%2F00000000-0000-4000-8000-000000000000%2F&commonsHostOrigin=https%3A%2F%2Fagentcommons.io",
],
];

for (const [path, init] of publicRoutes) {
await expectStatus(`public ${init?.method ?? "GET"} ${path}`, app.request(path, init), 503);
await expectStatus(
`public ${init?.method ?? "GET"} ${path}`,
app.request(path, init),
503,
);
}

/** Routes that carry user data and must stay behind the credential check. */
Expand All @@ -61,6 +97,30 @@ for (const path of protectedRoutes) {
await expectStatus(`protected GET ${path}`, app.request(path), 401);
}

const previewResponse = await app.request("/v1/previews/example-project/");
if (previewResponse.headers.has("x-frame-options")) {
failures.push("public preview: gateway must not add x-frame-options");
}
if (previewResponse.headers.get("access-control-allow-origin") !== "*") {
failures.push("public preview: sandboxed modules require wildcard CORS");
}
if (previewResponse.headers.has("access-control-allow-credentials")) {
failures.push("public preview: public assets must not allow credentials");
}

const pluginHostResponse = await app.request(
"/v1/ui-plugin-host?entry=https%3A%2F%2Fapi.agentcommons.io%2Fv1%2Fpreviews%2Fexample-project%2Fdeployments%2F00000000-0000-4000-8000-000000000000%2F&commonsHostOrigin=https%3A%2F%2Fagentcommons.io",
);
if (pluginHostResponse.headers.has("x-frame-options")) {
failures.push("UI plugin host: gateway must not add x-frame-options");
}
if (pluginHostResponse.headers.get("access-control-allow-origin") !== "*") {
failures.push("UI plugin host: opaque sandbox relay requires wildcard CORS");
}
if (pluginHostResponse.headers.has("access-control-allow-credentials")) {
failures.push("UI plugin host: must not allow credentials");
}

if (failures.length > 0) {
console.error("Gateway smoke test failed:");
for (const failure of failures) console.error(` - ${failure}`);
Expand Down
129 changes: 109 additions & 20 deletions apps/commons-api-gateway/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,37 @@ export function createGatewayApp() {
const app = new Hono<{ Variables: Variables }>();
const counters = new Map<string, { minute: number; count: number }>();

app.use("*", secureHeaders());
app.use(
"*",
cors({
origin: (origin) => {
const allowed = (process.env.CORS_ORIGINS ?? "")
.split(",")
.map((value) => value.trim());
return allowed.includes(origin) ? origin : allowed[0] ?? "";
},
allowHeaders: [
"authorization",
"content-type",
"idempotency-key",
"x-request-id",
],
exposeHeaders: ["x-request-id", "x-commons-service"],
}),
);
const securityHeaders = secureHeaders();
app.use("*", (c, next) => {
// Published code-project previews provide their own deliberately strict
// sandbox policy. The gateway default includes X-Frame-Options:
// SAMEORIGIN, which prevents these cross-origin previews from loading in
// the Commons plugin iframe.
if (isPluginFramePath(c.req.path)) return next();
return securityHeaders(c, next);
});
const crossOriginHeaders = cors({
origin: (origin) => {
const allowed = (process.env.CORS_ORIGINS ?? "")
.split(",")
.map((value) => value.trim());
return allowed.includes(origin) ? origin : (allowed[0] ?? "");
},
allowHeaders: [
"authorization",
"content-type",
"idempotency-key",
"x-request-id",
],
exposeHeaders: ["x-request-id", "x-commons-service"],
});
app.use("*", (c, next) => {
// A sandboxed plugin frame intentionally has an opaque `null` origin.
// Public preview modules therefore need wildcard CORS and no credentials;
// the preview proxy below applies that narrower policy.
if (isPluginFramePath(c.req.path)) return next();
return crossOriginHeaders(c, next);
});
app.use("*", async (c, next) => {
const requestId = c.req.header("x-request-id") ?? `req_${randomUUID()}`;
c.set("requestId", requestId);
Expand All @@ -57,6 +69,11 @@ export function createGatewayApp() {
baseUrl: string | undefined,
targetPath: string,
) {
const isPluginFrame = isPluginFramePath(targetPath);
if (isPluginFrame) {
c.header("access-control-allow-origin", "*");
c.header("cross-origin-resource-policy", "cross-origin");
}
if (!baseUrl) {
return c.json(
{
Expand All @@ -72,7 +89,9 @@ export function createGatewayApp() {
const url = new URL(targetPath, `${baseUrl.replace(/\/$/, "")}/`);
const incoming = new URL(c.req.url);
url.search = incoming.search;
const headers = new Headers(c.req.raw.headers);
const headers = isPluginFrame
? publicAssetRequestHeaders(c.req.raw.headers)
: new Headers(c.req.raw.headers);
headers.delete("host");
headers.delete("content-length");
headers.delete("authorization");
Expand All @@ -88,6 +107,11 @@ export function createGatewayApp() {
duplex: "half",
} as RequestInit);
const outputHeaders = new Headers(response.headers);
if (isPluginFrame) {
outputHeaders.set("access-control-allow-origin", "*");
outputHeaders.delete("access-control-allow-credentials");
outputHeaders.set("cross-origin-resource-policy", "cross-origin");
}
outputHeaders.set("x-request-id", c.get("requestId"));
outputHeaders.set("x-commons-service", service);
return new Response(response.body, {
Expand Down Expand Up @@ -142,6 +166,30 @@ export function createGatewayApp() {
c.req.path,
),
);
// Published code projects are intentionally public, unguessable preview
// assets. The upstream only serves projects whose visibility is public and
// whose latest deployment is ready. They must bypass credential auth so an
// isolated iframe can load HTML and relative assets without receiving a
// Commons bearer token.
app.get("/v1/previews/*", (c) =>
publicProxy(
c,
"agent-commons",
process.env.AGENT_COMMONS_INTERNAL_URL,
c.req.path,
),
);
// Trusted relay around an opaque generated-app iframe. It carries no user
// data or credential and must be frameable from the configured Commons app
// origin, just like the immutable preview it contains.
app.get("/v1/ui-plugin-host", (c) =>
publicProxy(
c,
"agent-commons",
process.env.AGENT_COMMONS_INTERNAL_URL,
c.req.path,
),
);

app.use("/v1/*", async (c, next) => {
const principal = await authenticate(c.req.header("authorization"));
Expand Down Expand Up @@ -356,6 +404,47 @@ if (process.env.COMMONS_GATEWAY_NO_LISTEN !== "true") {

export default app;

function isPluginFramePath(path: string) {
return (
path === "/v1/ui-plugin-host" ||
path === "/v1/previews" ||
path.startsWith("/v1/previews/")
);
}

/**
* Public preview and relay responses never need a Commons identity. Strip all
* ambient browser credentials and gateway delegation headers before the
* request reaches the API service. This keeps the public content origin
* cookieless even when it currently shares the public API hostname.
*/
export function publicAssetRequestHeaders(input: HeadersInit) {
const headers = new Headers(input);
for (const name of [
"host",
"content-length",
"authorization",
"proxy-authorization",
"cookie",
"x-owner-id",
"x-initiator",
"x-user-id",
"x-user-email",
"x-commons-actor-id",
"x-commons-actor-type",
"x-commons-workspace-id",
"x-commons-project-id",
"x-commons-scopes",
"x-commons-request-id",
"x-commons-timestamp",
"x-commons-signature",
"x-commons-internal-secret",
]) {
headers.delete(name);
}
return headers;
}

function requiredScope(method: string, path: string) {
if (path.includes("/activity")) return "activity:read";
if (path.startsWith("/v1/compute")) {
Expand Down
27 changes: 27 additions & 0 deletions apps/commons-api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ BRAVE_SEARCH_COST_USD_PER_CALL="0.005"
# SEARXNG_API_KEY=""
# SEARXNG_SEARCH_COST_USD_PER_CALL="0"
OPENAI_TRANSCRIPTION_COST_USD_PER_MINUTE="0.003"
# Video uploads are sampled with ffmpeg and summarized by a multimodal model.
# Set to false to retain videos without automatic understanding.
AGENT_FILE_VIDEO_UNDERSTANDING_ENABLED="true"
AGENT_FILE_VIDEO_UNDERSTANDING_MODEL="gpt-5.4-mini"
AGENT_FILE_VIDEO_MAX_FRAMES="8"
FFMPEG_PATH="ffmpeg"
# Audio-only transcription remains opt-in. Video audio is transcribed as part
# of enabled video understanding.
AGENT_FILE_AUDIO_TRANSCRIPTION_ENABLED="false"
AGENT_FILE_AUDIO_TRANSCRIPTION_MODEL="gpt-4o-mini-transcribe"
OPENAI_TTS_COST_USD_PER_1K_CHARACTERS="0.015"
ELEVENLABS_TTS_COST_USD_PER_1K_CHARACTERS="0.10"
# Optional JSON override keyed by quality then size; defaults track GPT Image 2.
Expand All @@ -113,3 +123,20 @@ BILLING_ENFORCEMENT="true"
# Optional JSON override of credits/min per profile, e.g.
# {"starter":2,"standard":7,"performance":14,"gpu":70}
COMPUTE_CREDITS_PER_MIN=""

# ── Provenance ───────────────────────────────────────────────────────────
# Metadata capture writes hashes, sizes, timings and attribution without raw
# prompt/output text. Full capture is a per-run user choice. Hidden model
# reasoning is never persisted by this subsystem.
PROVENANCE_DEFAULT_MODE="metadata"
PROVENANCE_FULL_CAPTURE_ENABLED="true"
# On-chain submission is both environment-gated and explicitly requested.
PROVENANCE_ONCHAIN_ENABLED="false"
# Optional asynchronous ProvenanceKit sink. Local trajectory recording works
# without it and never waits on this service.
PROVENANCEKIT_EXPORT_ENABLED="false"
PROVENANCEKIT_API_URL=""
PROVENANCEKIT_API_KEY=""
PROVENANCE_BATCH_SIZE="200"
PROVENANCE_FLUSH_MS="40"
PROVENANCE_QUEUE_LIMIT="5000"
Loading
Loading