Skip to content
Merged
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
1 change: 1 addition & 0 deletions agents/myra/src/definition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const INPUT = {
triggerAddress: "ins_dep000000000000@example.test",
inferencePreferences: [{ provider: "anthropic", model: "claude-test" }],
systemPrompt: "You are Myra.",
hubCredentialId: "crd_000000000000000000000000000000ab",
} as const;

function assistantStep(definition: WorkflowDefinition): StepPrimitive {
Expand Down
15 changes: 14 additions & 1 deletion agents/myra/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ import { mail } from "@intx/tools-mail/sidecar-bundle";
import { posix } from "@intx/tools-posix/sidecar-bundle";
import { artifacts } from "@corbits/artifacts/sidecar-bundle";

import { ASSISTANT_STEP_ID } from "./workflow-ids";
import {
ASSISTANT_STEP_ID,
artifactToolsCredentialBinding,
artifactToolsCredentialUseRequirement,
} from "./workflow-ids";

export { ASSISTANT_SYSTEM_PROMPT } from "./system-prompt";
export { ASSISTANT_STEP_ID, ASSISTANT_WORKFLOW_ID } from "./workflow-ids";
Expand Down Expand Up @@ -47,6 +51,10 @@ export interface MyraWorkflowInput {
readonly inferencePreferences: readonly InferencePreference[];
/** The prompt this deployment runs with. */
readonly systemPrompt: string;
/** The tenant credential holding this agent's hub token, minted by the
* deployer before the source is pushed. The definition binds it to the
* artifact tools and requires its use on the deployer's authority. */
readonly hubCredentialId: string;
}

/**
Expand All @@ -71,9 +79,14 @@ export function buildMyraWorkflow(input: MyraWorkflowInput): WorkflowDefinition
if (input.systemPrompt === "") {
throw new Error("buildMyraWorkflow requires a non-empty systemPrompt");
}
if (input.hubCredentialId === "") {
throw new Error("buildMyraWorkflow requires a non-empty hubCredentialId");
}
return defineWorkflow({
id: input.workflowId,
trigger: { type: "mail", to: input.triggerAddress },
credentialBindings: [artifactToolsCredentialBinding(input.workflowId)],
grantRequirements: [artifactToolsCredentialUseRequirement(input.hubCredentialId)],
steps: {
assistant: step({
agent: {
Expand Down
61 changes: 59 additions & 2 deletions agents/myra/src/workflow-ids.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,62 @@
// The workflow/step ids for Myra's mail-triggered assistant definition.
// Split from the prompt so the prompt module stays prompt-only.
// The workflow/step ids for Myra's mail-triggered assistant definition, and
// the hub-credential facts a deployer and the definition must agree on.
// Split from the prompt so the prompt module stays prompt-only, and kept
// free of the runtime so a browser can import it.

export const ASSISTANT_WORKFLOW_ID = "wf_assistant";
export const ASSISTANT_STEP_ID = "assistant";

/** The tool package the hub credential is bound to; also the consumer the
* credential-use grant is conditioned on. */
export const ARTIFACT_TOOLS_PACKAGE = "@corbits/artifacts/sidecar-bundle";

/** The handle the artifact tools resolve at run time. */
export const HUB_CREDENTIAL_HANDLE = "hub";

/** The provider row standing for the hub itself, one per workbench. */
export const HUB_PROVIDER_NAME = "workbench-hub";

/** The credential name an agent's hub token is stored under; the binding
* resolves it by this name, so both sides derive it here. */
export function agentHubCredentialName(workflowId: string): string {
return `${workflowId}-hub`;
}

/** The definition's credential binding: the deploy resolves the named
* tenant-owned credential into the artifact tools' `hub` handle. */
export function artifactToolsCredentialBinding(workflowId: string): {
readonly package: string;
readonly handle: string;
readonly provider: string;
readonly name: string;
readonly locator: "tenant";
} {
return {
package: ARTIFACT_TOOLS_PACKAGE,
handle: HUB_CREDENTIAL_HANDLE,
provider: HUB_PROVIDER_NAME,
name: agentHubCredentialName(workflowId),
locator: "tenant",
};
}

/** The definition's grant requirement: at the run's first trigger the hub
* resolves it against the definition creator's authority and stamps the
* `credential:{id}` / `use` grant the artifact tools' runtime gate checks,
* scoped to that one package. The run principal does not exist before
* then, so this is the only place the grant can be declared. */
export function artifactToolsCredentialUseRequirement(credentialId: string): {
readonly resource: string;
readonly action: "use";
readonly effect: "allow";
readonly source: "creator";
readonly conditions: { readonly tool: string };
} {
return {
resource: `credential:${credentialId}`,
action: "use",
effect: "allow",
source: "creator",
conditions: { tool: `tool:${ARTIFACT_TOOLS_PACKAGE}` },
};
}
2 changes: 2 additions & 0 deletions apps/web/src/agent-deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe("buildAgentDefinitionJson", () => {
systemPrompt: "You research things.",
triggerAddress: "research-buddy@example.test",
declaredSources: [{ provider: "anthropic", model: "claude-test" }],
hubCredentialId: "crd_000000000000000000000000000000ab",
};
const projection = buildAgentDefinitionJson(args) as {
id: string;
Expand All @@ -27,6 +28,7 @@ describe("buildAgentDefinitionJson", () => {
triggerAddress: args.triggerAddress,
inferencePreferences: args.declaredSources,
systemPrompt: args.systemPrompt,
hubCredentialId: args.hubCredentialId,
};

expect(projection.id).toBe(buildInput.workflowId);
Expand Down
33 changes: 21 additions & 12 deletions apps/web/src/agent-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import { reportError } from "@corbits/error-sink";

import {
artifactToolsCredentialBinding,
ensureAgentHubCredential,
grantArtifactToolsCredentialUse,
} from "./agent-hub-credential";
artifactToolsCredentialUseRequirement,
} from "@corbits/myra/workflow-ids";

import { ensureAgentHubCredential } from "./agent-hub-credential";
import { resolveExistingOffering } from "./onboarding/provider-connect-step";
import { isValidSlug, slugify } from "@/lib/slug";

Expand Down Expand Up @@ -93,15 +94,18 @@ export function buildAgentDefinitionJson(args: {
systemPrompt: string;
triggerAddress: string;
declaredSources: readonly { readonly provider: string; readonly model: string }[];
hubCredentialId: string;
}): unknown {
const stepId = "run";
return {
id: args.slug,
// `to` only feeds the deploy-time mail.address/mail.send grants; it is
// not how mail reaches this agent — that happens at its run address.
triggers: [{ type: "mail", to: args.triggerAddress }],
// Resolved at deploy into the `hub` handle the artifact tools use.
// Resolved at deploy into the `hub` handle the artifact tools use, and
// granted to the run on the deployer's authority at its first trigger.
credentialBindings: [artifactToolsCredentialBinding(args.slug)],
grantRequirements: [artifactToolsCredentialUseRequirement(args.hubCredentialId)],
steps: {
[stepId]: {
kind: "step",
Expand Down Expand Up @@ -171,6 +175,7 @@ export async function pushAgentSource(
readonly systemPrompt: string;
readonly triggerAddress: string;
readonly declaredSources: readonly { readonly provider: string; readonly model: string }[];
readonly hubCredentialId: string;
},
fetchImpl: typeof fetch = fetch,
): Promise<string> {
Expand All @@ -184,13 +189,15 @@ export async function pushAgentSource(
triggerAddress: args.triggerAddress,
inferencePreferences: args.declaredSources.map((source) => ({ ...source })),
systemPrompt: args.systemPrompt,
hubCredentialId: args.hubCredentialId,
},
workflowJson: JSON.stringify(
buildAgentDefinitionJson({
slug: args.slug,
systemPrompt: args.systemPrompt,
triggerAddress: args.triggerAddress,
declaredSources: args.declaredSources,
hubCredentialId: args.hubCredentialId,
}),
),
});
Expand Down Expand Up @@ -345,9 +352,9 @@ export async function deployAgentSource(
}

const assetId = await ensureAgentSourceAsset(args.tenantId, assetName, name, fetchImpl);
// Minted before the push: the definition's credential binding resolves
// this credential by name at deploy time, so it must already exist.
const credentialId = await ensureAgentHubCredential(
// Minted before the push: the definition binds this credential by name
// and requires its use by id, so it must exist before the source does.
const hubCredentialId = await ensureAgentHubCredential(
{ tenantId: args.tenantId, definitionId: slug, assetId },
fetchImpl,
);
Expand All @@ -359,7 +366,13 @@ export async function deployAgentSource(
assetId,
assetName,
packageName,
{ slug, systemPrompt, triggerAddress, declaredSources: offering.declaredSources },
{
slug,
systemPrompt,
triggerAddress,
declaredSources: offering.declaredSources,
hubCredentialId,
},
fetchImpl,
);

Expand All @@ -383,10 +396,6 @@ export async function deployAgentSource(
if (parsed instanceof type.errors) {
throw new AgentDeployError(`this deployment came back an unexpected shape: ${parsed.summary}`);
}
await grantArtifactToolsCredentialUse(
{ tenantId: args.tenantId, deploymentId: parsed.id, credentialId },
fetchImpl,
);
if (args.input.schedule !== undefined) {
await scheduleAgentRun(
args.tenantId,
Expand Down
Loading
Loading