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
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ change follows, whether a person or a coding agent writes it.
## Releasing

Maintainers bump `version` in the root, `apps/hub`, `apps/web`,
`packages/client` and `packages/schemas` package.json files, run
`packages/client`, `packages/schemas` and `packages/tools-hub` package.json
files, run
`bun install`, and merge. Once the publish workflow for that commit on main has
finished, `git tag -s vX.Y.Z && git push origin vX.Y.Z` retags its hub and
sidecar images as `X.Y.Z` and publishes the GitHub release; a 0.x version or
Expand Down
1 change: 1 addition & 0 deletions apps/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@corbits-builder/designer": "workspace:*",
"@corbits-builder/reviewer": "workspace:*",
"@corbits-builder/schemas": "workspace:*",
"@corbits-builder/tools-hub": "workspace:*",
"@corbits-builder/writer": "workspace:*",
"@corbits/agent-token": "0.2.2",
"@corbits/artifacts": "0.3.0",
Expand Down
6 changes: 4 additions & 2 deletions apps/hub/src/artifacts/design-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { DesignBundle } from "@corbits-builder/schemas/artifact";
import { designCheckFor } from "../builds/design-check.ts";
import { withDesignTokens } from "./design-tokens.ts";
import { DESIGN_STYLES } from "./design-tweaks.ts";
import { resolveWorkflowProjectScope } from "../workflows/project-scope.ts";
import { workflowHttpJson } from "../workflows/http-json.ts";
import {
resolveWorkflowProjectScope,
workflowHttpJson,
} from "@corbits-builder/tools-hub";
import { failure, HubArtifactReceipt } from "./tools.ts";

interface DesignToolEnv extends BaseEnv {
Expand Down
7 changes: 4 additions & 3 deletions apps/hub/src/artifacts/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import {
} from "@corbits-builder/schemas/artifact";
import type { WorkflowRole } from "@corbits-builder/schemas/installed-workflows";
import { unpackDesign } from "./design-tools.ts";
import { projectScopeFailure } from "../workflows/project-scope.ts";
import {
projectScopeFailure,
resolveWorkflowProjectScope,
} from "../workflows/project-scope.ts";
import { workflowHttpJson, WorkflowHttpError } from "../workflows/http-json.ts";
workflowHttpJson,
WorkflowHttpError,
} from "@corbits-builder/tools-hub";

interface ArtifactToolEnv extends BaseEnv {
capabilities: RuntimeCapabilities;
Expand Down
2 changes: 1 addition & 1 deletion apps/hub/src/builds/app-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
mediatedHttpJson,
WorkflowHttpError,
workflowHttpJson,
} from "../workflows/http-json.ts";
} from "@corbits-builder/tools-hub";

const AppPushAccess = type({
path: "string",
Expand Down
5 changes: 4 additions & 1 deletion apps/hub/src/builds/app-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import {
DesignDraft,
MAX_APP_BYTES,
} from "@corbits-builder/schemas/artifact";
import { WorkflowHttpError, workflowHttpJson } from "../workflows/http-json.ts";
import {
WorkflowHttpError,
workflowHttpJson,
} from "@corbits-builder/tools-hub";
import { INPUTS_DIRECTORY } from "../config/inputs.ts";

// Installs and builds are rebuilt by setup and check-app, and .builder and the inputs come from
Expand Down
6 changes: 4 additions & 2 deletions apps/hub/src/builds/sidecar-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import {
type ToolRunner,
} from "@intx/types/runtime";
import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities";
import { resolveWorkflowProjectScope } from "../workflows/project-scope.ts";
import { WorkflowHttpError } from "../workflows/http-json.ts";
import {
resolveWorkflowProjectScope,
WorkflowHttpError,
} from "@corbits-builder/tools-hub";
import { APP_STATUS_TOOL, createCodingTools } from "./coding-tools.ts";
import { createPosixBuildExecutor } from "./posix-sandbox.ts";
import { browserTools } from "./browser.ts";
Expand Down
7 changes: 4 additions & 3 deletions apps/hub/src/projects/deploy-tools.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { projectScopeFailure } from "../workflows/project-scope.ts";
import {
projectScopeFailure,
resolveWorkflowProjectScope,
} from "../workflows/project-scope.ts";
workflowHttpJson,
WorkflowHttpError,
} from "@corbits-builder/tools-hub";
import {
defineTool,
type BaseEnv,
Expand Down Expand Up @@ -50,7 +52,6 @@ import {
designLookIdeas,
} from "../config/design.ts";
import { QUESTIONS_ASKED } from "../config/questions.ts";
import { workflowHttpJson, WorkflowHttpError } from "../workflows/http-json.ts";
import { assignmentToWriterMessage } from "../workflows/writer-task.ts";
import { coderTaskMessage } from "@corbits-builder/coder/contract";

Expand Down
38 changes: 2 additions & 36 deletions apps/hub/src/workflows/project-scope.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,13 @@
import { HUB_CREDENTIAL_HANDLE } from "@intx/types/credential-delivery";
import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities";
import {
WorkflowProjectScope,
type WorkflowRole,
} from "@corbits-builder/schemas/installed-workflows";
import { getLogger } from "@intx/log";
import { projectScopeCode } from "@corbits-builder/tools-hub";
import { requestErrorMetadata } from "../request-error.ts";
import { WorkflowHttpError, workflowHttpJson } from "./http-json.ts";

const log = getLogger(["corbits-builder", "hub", "workflow-project-scope"]);
const SAFE_CODE = /^[a-z0-9_:]+$/;
// A deployed agent keeps the bundle it started with; a field a newer hub adds must not refuse its scope.
const HubProjectScope = WorkflowProjectScope.onDeepUndeclaredKey("ignore");

// The model sees only the original code (never a refusal body or arbitrary message); the cause stays in the hub log.
export function projectScopeFailure(callId: string, error: unknown) {
log.error(
"Workflow project scope or tool call failed",
requestErrorMetadata(error),
);
const code =
error instanceof WorkflowHttpError && error.refusalCode
? error.refusalCode
: error instanceof Error && SAFE_CODE.test(error.message)
? error.message
: "workflow_project_scope_unavailable";
return { callId, content: code, isError: true };
}

export async function resolveWorkflowProjectScope(
capabilities: RuntimeCapabilities,
role: WorkflowRole,
signal: AbortSignal,
) {
const scope = HubProjectScope.assert(
await workflowHttpJson({
capabilities,
handle: HUB_CREDENTIAL_HANDLE,
path: "/api/workflow-project-scope",
maxResponseBytes: 16384,
signal,
}),
);
if (scope.role !== role) throw new Error("workflow_project_role_refused");
return scope;
return { callId, content: projectScopeCode(error), isError: true };
}
11 changes: 11 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/DEV.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ running with Docker Compose is in
| `apps/web` | The UI: Vite and React over that client, with its own small router in `src/routes.ts`. |
| `workflows/*` | One package per role: its agent, prompt and workflow definition; the hub supplies its tools. |
| `packages/schemas` | Types and schemas shared by the hub, the client and the workflows. |
| `packages/tools-hub` | The helpers the hub's agent tools use to call the hub and resolve a run's project (`@corbits-builder/tools-hub`). |
| `scripts/` | Dev and verification scripts: pin and vendor checks, the commit check, the Google sign-in emulator and React UI API generation. |
| `deploy/` | The Dockerfile and Compose files ([SELF_HOST.md](https://github.com/corbitsdev/corbits-builder/blob/main/docs/SELF_HOST.md)). |

Expand Down
1 change: 1 addition & 0 deletions docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ Environment files, installs and build output never leave the workspace.
| `workflows/designer` | `@corbits-builder/designer` | The designer, its starter, themes and skills. |
| `workflows/coder` | `@corbits-builder/coder` | The coder, its app starter and `check-app`. |
| `packages/schemas` | `@corbits-builder/schemas` | Types and schemas shared by the hub, client and workflows. |
| `packages/tools-hub` | `@corbits-builder/tools-hub` | The hub HTTP/JSON and project-scope helpers agent tools use. |
| `vendor/interchange` | | Interchange at the pinned revision. |
| `deploy/` | | The Dockerfile and Compose files. |
| `scripts/` | | Dev and verification scripts. |
Expand Down
30 changes: 30 additions & 0 deletions packages/tools-hub/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Tools hub

`@corbits-builder/tools-hub`: the helpers agent tools use to reach the
hub from inside a run. `workflowHttpJson` calls a hub route through the run's
mediated hub credential and throws `WorkflowHttpError` on a refusal,
`resolveWorkflowProjectScope` reads the project a run belongs to, and
`projectScopeCode` turns a failure into the only code the model sees.

## Where it fits

Pure functions with no tool definitions and no runtime of their own. It
imports only [`packages/schemas`][schemas] and Interchange's types. The
[hub][hub]'s agent tools use it, and the workflow bundles carry it with
those tools.

## Layout

- `src/index.ts`: the package's exports.
- `src/http-json.ts`: `workflowHttpJson`, `mediatedHttpJson` and
`WorkflowHttpError`.
- `src/project-scope.ts`: `resolveWorkflowProjectScope` and
`projectScopeCode`.

## Working on it

Refusal bodies never reach the model; keep any new field to a public code.
`bun run typecheck` checks every importer and `bun run test` runs the tests.

[hub]: https://github.com/corbitsdev/corbits-builder/tree/main/apps/hub
[schemas]: https://github.com/corbitsdev/corbits-builder/tree/main/packages/schemas
15 changes: 15 additions & 0 deletions packages/tools-hub/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@corbits-builder/tools-hub",
"version": "0.1.0-rc.2",
"private": true,
"license": "SEE LICENSE IN ../../LICENSE.md",
"author": "Sawyer Cutler <sawyer@dirtroad.dev>",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@corbits-builder/schemas": "workspace:*",
"@intx/types": "workspace:*"
}
}
9 changes: 9 additions & 0 deletions packages/tools-hub/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export {
mediatedHttpJson,
WorkflowHttpError,
workflowHttpJson,
} from "./http-json.ts";
export {
projectScopeCode,
resolveWorkflowProjectScope,
} from "./project-scope.ts";
17 changes: 17 additions & 0 deletions packages/tools-hub/src/project-scope.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { expect, test } from "bun:test";
import { WorkflowHttpError } from "./http-json.ts";
import { projectScopeCode } from "./project-scope.ts";

test("project scope failures expose only a refusal code, a safe code or the fallback", () => {
expect(
projectScopeCode(
new WorkflowHttpError(403, "unspecified", "project_access_refused"),
),
).toBe("project_access_refused");
expect(projectScopeCode(new Error("workflow_project_role_refused"))).toBe(
"workflow_project_role_refused",
);
expect(projectScopeCode(new Error("ENOENT: /secret/path"))).toBe(
"workflow_project_scope_unavailable",
);
});
39 changes: 39 additions & 0 deletions packages/tools-hub/src/project-scope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { HUB_CREDENTIAL_HANDLE } from "@intx/types/credential-delivery";
import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities";
import {
WorkflowProjectScope,
type WorkflowRole,
} from "@corbits-builder/schemas/installed-workflows";
import { WorkflowHttpError, workflowHttpJson } from "./http-json.ts";

// A deployed agent keeps the bundle it started with; a field a newer hub adds must not refuse its scope.
const HubProjectScope = WorkflowProjectScope.onDeepUndeclaredKey("ignore");

export async function resolveWorkflowProjectScope(
capabilities: RuntimeCapabilities,
role: WorkflowRole,
signal: AbortSignal,
) {
const scope = HubProjectScope.assert(
await workflowHttpJson({
capabilities,
handle: HUB_CREDENTIAL_HANDLE,
path: "/api/workflow-project-scope",
maxResponseBytes: 16384,
signal,
}),
);
if (scope.role !== role) throw new Error("workflow_project_role_refused");
return scope;
}

const SAFE_CODE = /^[a-z0-9_:]+$/;

// The model sees only the original code (never a refusal body or arbitrary message); the cause stays in the hub log.
export function projectScopeCode(error: unknown): string {
if (error instanceof WorkflowHttpError && error.refusalCode)
return error.refusalCode;
if (error instanceof Error && SAFE_CODE.test(error.message))
return error.message;
return "workflow_project_scope_unavailable";
}