@openshain/core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +4 -0
- package/dist/authority/policy.d.ts +131 -0
- package/dist/authority/policy.js +335 -0
- package/dist/config/load.js +4 -41
- package/dist/config/schema.d.ts +12 -9
- package/dist/config/schema.js +14 -10
- package/dist/config/yaml.d.ts +12 -0
- package/dist/config/yaml.js +49 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/runtime.d.ts +28 -3
- package/dist/runtime.js +148 -6
- package/dist/schemas.d.ts +1 -1
- package/dist/schemas.js +3 -0
- package/dist/tool/ask-user.d.ts +5 -0
- package/dist/tool/ask-user.js +22 -0
- package/dist/tool/paths.d.ts +1 -1
- package/dist/tool/paths.js +1 -1
- package/dist/tool/types.js +6 -0
- package/dist/work/events.d.ts +150 -1
- package/dist/work/events.js +146 -0
- package/dist/work/history.d.ts +57 -0
- package/dist/work/history.js +81 -0
- package/dist/work/projection.js +21 -6
- package/package.json +3 -2
- package/src/authority/policy.ts +400 -0
- package/src/config/load.ts +4 -43
- package/src/config/schema.ts +41 -31
- package/src/config/yaml.ts +60 -0
- package/src/index.ts +43 -1
- package/src/runtime.ts +189 -10
- package/src/schemas.ts +17 -1
- package/src/tool/ask-user.ts +25 -0
- package/src/tool/paths.ts +1 -1
- package/src/tool/types.ts +6 -0
- package/src/work/events.ts +192 -0
- package/src/work/history.ts +121 -0
- package/src/work/projection.ts +21 -6
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isNode, LineCounter, parseDocument } from "yaml";
|
|
2
|
+
import { OpenshainError } from "../errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* Parses a YAML file against a zod schema. Every problem is reported with its line and column
|
|
5
|
+
* and the path of the field, so that a person can fix the file. Returns the data together with
|
|
6
|
+
* `problem`, for checks the caller adds after parsing.
|
|
7
|
+
*/
|
|
8
|
+
export function parseYamlFile(text, schema, fileName) {
|
|
9
|
+
const lineCounter = new LineCounter();
|
|
10
|
+
let doc;
|
|
11
|
+
let data;
|
|
12
|
+
try {
|
|
13
|
+
doc = parseDocument(text, { lineCounter });
|
|
14
|
+
data = doc.errors.length > 0 ? undefined : doc.toJS();
|
|
15
|
+
}
|
|
16
|
+
catch (cause) {
|
|
17
|
+
// yaml refuses resource-exhaustion documents (alias bombs) with a plain error
|
|
18
|
+
throw new OpenshainError("config", `${fileName}: ${cause.message}`, { cause });
|
|
19
|
+
}
|
|
20
|
+
if (doc.errors.length > 0) {
|
|
21
|
+
const lines = doc.errors.map((error) => {
|
|
22
|
+
const pos = error.linePos?.[0] ?? { line: 0, col: 0 };
|
|
23
|
+
return `${fileName}:${pos.line}:${pos.col} ${firstLine(error.message)}`;
|
|
24
|
+
});
|
|
25
|
+
throw new OpenshainError("config", lines.join("\n"));
|
|
26
|
+
}
|
|
27
|
+
const locate = (path) => {
|
|
28
|
+
for (let i = path.length; i >= 0; i--) {
|
|
29
|
+
const node = i === 0 ? doc.contents : doc.getIn(path.slice(0, i), true);
|
|
30
|
+
if (isNode(node) && node.range)
|
|
31
|
+
return lineCounter.linePos(node.range[0]);
|
|
32
|
+
}
|
|
33
|
+
return { line: 1, col: 1 };
|
|
34
|
+
};
|
|
35
|
+
const problem = (path, message) => {
|
|
36
|
+
const { line, col } = locate(path);
|
|
37
|
+
const where = path.length === 0 ? "<root>" : path.map(String).join(".");
|
|
38
|
+
return `${fileName}:${line}:${col} ${where}: ${message}`;
|
|
39
|
+
};
|
|
40
|
+
const result = schema.safeParse(data);
|
|
41
|
+
if (!result.success) {
|
|
42
|
+
const problems = result.error.issues.map((issue) => problem(issue.path, issue.message));
|
|
43
|
+
throw new OpenshainError("config", problems.join("\n"));
|
|
44
|
+
}
|
|
45
|
+
return { data: result.data, problem };
|
|
46
|
+
}
|
|
47
|
+
function firstLine(message) {
|
|
48
|
+
return message.split("\n", 1)[0] ?? message;
|
|
49
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
export { AUTHORITY_DIR_NAME, type Authority, type AuthorityRequest, DECISION_KINDS, DECISIONS_DIR_NAME, DELEGATIONS_FILE_NAME, type Decision, DecisionFileSchema, type DecisionKind, type DecisionRecord, type Delegation, DelegationsFileSchema, evaluate, loadAuthority, matchGlob, OPEN_AUTHORITY, POLICY_FILE_NAME, type PolicyFile, PolicyFileSchema, type Rule, writeDecision, } from "./authority/policy.ts";
|
|
1
2
|
export { CONFIG_FILE_NAME, loadConfig, type ParseConfigOptions, parseConfig, } from "./config/load.ts";
|
|
2
|
-
export type { Config, ToolProviderRef } from "./config/schema.ts";
|
|
3
|
+
export type { Config, ModelConfig, ToolProviderRef } from "./config/schema.ts";
|
|
3
4
|
export { LANGUAGES, type Language } from "./config/schema.ts";
|
|
4
5
|
export { ERROR_CODES, type ErrorCode, isOpenshainError, OpenshainError } from "./errors.ts";
|
|
5
6
|
export { type EventId, newEventId, newWorkId, parseEventId, parseWorkId, type WorkId, } from "./ids.ts";
|
|
6
7
|
export type { ModelDescription, ModelMessage, ModelProvider, ModelRequest, ModelResponse, UserPart, } from "./model/types.ts";
|
|
7
|
-
export { type CreateRuntimeOptions, createRuntime, createToolCaller, createToolRegistry, MAX_TOOL_TEXT_CHARS, type Runtime, type RuntimeProviders, type ToolSummary, } from "./runtime.ts";
|
|
8
|
+
export { type CallOptions, type CreateRuntimeOptions, createRuntime, createToolCaller, createToolRegistry, MAX_TOOL_TEXT_CHARS, type PendingApprovalResult, REVIEW_DIR_NAME, type Runtime, type RuntimeProviders, type ToolSummary, } from "./runtime.ts";
|
|
8
9
|
export { jsonSchemas, type SchemaName } from "./schemas.ts";
|
|
10
|
+
export { ASK_USER, RUNTIME_PROVIDER_ID } from "./tool/ask-user.ts";
|
|
9
11
|
export { loadToolModule } from "./tool/load-module.ts";
|
|
10
12
|
export { RESERVED_PATHS, resolveWorkspacePath } from "./tool/paths.ts";
|
|
11
13
|
export { type HiddenTool, type RegisteredTool, type RegisterOptions, ToolRegistry, } from "./tool/registry.ts";
|
|
@@ -14,7 +16,8 @@ export { compileInputValidator, type InputValidation } from "./tool/validate.ts"
|
|
|
14
16
|
export { uuidv7 } from "./uuid.ts";
|
|
15
17
|
export { verifyArtifact } from "./work/artifacts.ts";
|
|
16
18
|
export { EVENTS_FILE_NAME, EventLog, type NewEvent } from "./work/event-log.ts";
|
|
17
|
-
export { type AnyEvent, type Artifact, type AssistantPart, canonical, type Event, type EventFile, EventFileSchema, type EventPayloads, type EventType, eventFromFile, eventToFile, type ModelUsage, payloadFileSchemas, type StopReason, TOOL_REJECTION_CODES, type ToolContent, type ToolRejectionCode, type UnknownEvent, } from "./work/events.ts";
|
|
19
|
+
export { type AnyEvent, type Artifact, type AssistantPart, canonical, type Event, type EventFile, EventFileSchema, type EventPayloads, type EventType, eventFromFile, eventToFile, isKnownEventType, type ModelUsage, parsePayloadFile, payloadFileSchemas, type ReviewPackage, type StopReason, TOOL_REJECTION_CODES, type ToolContent, type ToolRejectionCode, type UnknownEvent, } from "./work/events.ts";
|
|
20
|
+
export { countToolCalls, type FailureReason, type HistoryCall, type PendingApproval, type PendingQuestion, pendingApprovals, pendingQuestions, type WorkHistory, workHistory, } from "./work/history.ts";
|
|
18
21
|
export { acquireLock, LOCK_FILE_NAME, type Lock } from "./work/lock.ts";
|
|
19
22
|
export { buildProjection, type Projection, type ProjectionInput } from "./work/projection.ts";
|
|
20
23
|
export { type CreateWorkInput, type ListResult, WORK_DIR_NAME, WORK_FILE_NAME, type WorkHandle, WorkStore, } from "./work/store.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// @openshain/core: Contracts (provider interfaces), fundamental objects, and the work runtime
|
|
2
|
+
export { AUTHORITY_DIR_NAME, DECISION_KINDS, DECISIONS_DIR_NAME, DELEGATIONS_FILE_NAME, DecisionFileSchema, DelegationsFileSchema, evaluate, loadAuthority, matchGlob, OPEN_AUTHORITY, POLICY_FILE_NAME, PolicyFileSchema, writeDecision, } from "./authority/policy.js";
|
|
2
3
|
export { CONFIG_FILE_NAME, loadConfig, parseConfig, } from "./config/load.js";
|
|
3
4
|
export { LANGUAGES } from "./config/schema.js";
|
|
4
5
|
export { ERROR_CODES, isOpenshainError, OpenshainError } from "./errors.js";
|
|
5
6
|
export { newEventId, newWorkId, parseEventId, parseWorkId, } from "./ids.js";
|
|
6
|
-
export { createRuntime, createToolCaller, createToolRegistry, MAX_TOOL_TEXT_CHARS, } from "./runtime.js";
|
|
7
|
+
export { createRuntime, createToolCaller, createToolRegistry, MAX_TOOL_TEXT_CHARS, REVIEW_DIR_NAME, } from "./runtime.js";
|
|
7
8
|
export { jsonSchemas } from "./schemas.js";
|
|
9
|
+
export { ASK_USER, RUNTIME_PROVIDER_ID } from "./tool/ask-user.js";
|
|
8
10
|
export { loadToolModule } from "./tool/load-module.js";
|
|
9
11
|
export { RESERVED_PATHS, resolveWorkspacePath } from "./tool/paths.js";
|
|
10
12
|
export { ToolRegistry, } from "./tool/registry.js";
|
|
@@ -13,7 +15,8 @@ export { compileInputValidator } from "./tool/validate.js";
|
|
|
13
15
|
export { uuidv7 } from "./uuid.js";
|
|
14
16
|
export { verifyArtifact } from "./work/artifacts.js";
|
|
15
17
|
export { EVENTS_FILE_NAME, EventLog } from "./work/event-log.js";
|
|
16
|
-
export { canonical, EventFileSchema, eventFromFile, eventToFile, payloadFileSchemas, TOOL_REJECTION_CODES, } from "./work/events.js";
|
|
18
|
+
export { canonical, EventFileSchema, eventFromFile, eventToFile, isKnownEventType, parsePayloadFile, payloadFileSchemas, TOOL_REJECTION_CODES, } from "./work/events.js";
|
|
19
|
+
export { countToolCalls, pendingApprovals, pendingQuestions, workHistory, } from "./work/history.js";
|
|
17
20
|
export { acquireLock, LOCK_FILE_NAME } from "./work/lock.js";
|
|
18
21
|
export { buildProjection } from "./work/projection.js";
|
|
19
22
|
export { WORK_DIR_NAME, WORK_FILE_NAME, WorkStore, } from "./work/store.js";
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Authority } from "./authority/policy.ts";
|
|
2
|
+
import type { Config, ModelConfig } from "./config/schema.ts";
|
|
2
3
|
import type { ModelProvider } from "./model/types.ts";
|
|
3
4
|
import type { HiddenTool } from "./tool/registry.ts";
|
|
4
5
|
import { ToolRegistry } from "./tool/registry.ts";
|
|
@@ -6,7 +7,7 @@ import type { ToolCall, ToolDefinition, ToolProvider, ToolResult } from "./tool/
|
|
|
6
7
|
import { type WorkHandle, WorkStore } from "./work/store.ts";
|
|
7
8
|
export interface RuntimeProviders {
|
|
8
9
|
/** Model providers by the id used in openshain.yaml. */
|
|
9
|
-
models: Record<string, (model:
|
|
10
|
+
models: Record<string, (model: ModelConfig) => ModelProvider>;
|
|
10
11
|
/** Tool providers by the id used in openshain.yaml. Modules are loaded from the config directly. */
|
|
11
12
|
tools: Record<string, () => ToolProvider>;
|
|
12
13
|
}
|
|
@@ -21,6 +22,8 @@ export interface ToolSummary {
|
|
|
21
22
|
}
|
|
22
23
|
/** Longer tool output is cut here so that one tool cannot flood the model's context. */
|
|
23
24
|
export declare const MAX_TOOL_TEXT_CHARS = 50000;
|
|
25
|
+
/** Where a work keeps the review packages a person sends to a reviewer. */
|
|
26
|
+
export declare const REVIEW_DIR_NAME = "review";
|
|
24
27
|
export interface Runtime {
|
|
25
28
|
readonly workspaceRoot: string;
|
|
26
29
|
readonly config: Config;
|
|
@@ -41,6 +44,28 @@ export declare function createToolCaller(input: {
|
|
|
41
44
|
registry: ToolRegistry;
|
|
42
45
|
config: Config;
|
|
43
46
|
workspaceRoot: string;
|
|
44
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Who may do what. Pass a function when it can change while the server runs, as it does when
|
|
49
|
+
* a reviewer writes a decision. Omitted: the workspace is open, as one without authority/ is.
|
|
50
|
+
*/
|
|
51
|
+
authority?: Authority | (() => Authority);
|
|
52
|
+
}): (work: WorkHandle, call: ToolCall, options?: CallOptions) => Promise<ToolResult>;
|
|
53
|
+
export interface CallOptions {
|
|
54
|
+
/** The approval that lets this call run: the policy is not consulted again. */
|
|
55
|
+
approvedBy?: string;
|
|
56
|
+
}
|
|
57
|
+
/** What a held call answers with: the client shows it to the person and the turn stops there. */
|
|
58
|
+
export interface PendingApprovalResult {
|
|
59
|
+
pending: "approval" | "review";
|
|
60
|
+
approval_id: string;
|
|
61
|
+
rule_id: string;
|
|
62
|
+
approvers?: string[];
|
|
63
|
+
reviewer?: {
|
|
64
|
+
role: string;
|
|
65
|
+
name?: string;
|
|
66
|
+
};
|
|
67
|
+
/** For a review: why the policy asks for one, when a cited decision did not cover the call. */
|
|
68
|
+
why?: string;
|
|
69
|
+
}
|
|
45
70
|
/** Registers the tool providers the config names: the caller's factories by id, and modules from the workspace. Needs no model. */
|
|
46
71
|
export declare function createToolRegistry(workspaceRoot: string, config: Config, tools: RuntimeProviders["tools"]): Promise<ToolRegistry>;
|
package/dist/runtime.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { evaluate, loadAuthority, OPEN_AUTHORITY, } from "./authority/policy.js";
|
|
1
4
|
import { loadConfig } from "./config/load.js";
|
|
2
5
|
import { isOpenshainError, OpenshainError } from "./errors.js";
|
|
3
6
|
import { loadToolModule } from "./tool/load-module.js";
|
|
4
7
|
import { ToolRegistry } from "./tool/registry.js";
|
|
8
|
+
import { uuidv7 } from "./uuid.js";
|
|
5
9
|
import { TOOL_REJECTION_CODES } from "./work/events.js";
|
|
6
|
-
import { WorkStore } from "./work/store.js";
|
|
10
|
+
import { WORK_DIR_NAME, WorkStore } from "./work/store.js";
|
|
7
11
|
/** Longer tool output is cut here so that one tool cannot flood the model's context. */
|
|
8
12
|
export const MAX_TOOL_TEXT_CHARS = 50_000;
|
|
13
|
+
/** Where a work keeps the review packages a person sends to a reviewer. */
|
|
14
|
+
export const REVIEW_DIR_NAME = "review";
|
|
9
15
|
/** Builds a runtime for one workspace from its config and the providers the caller knows. */
|
|
10
16
|
export async function createRuntime(options) {
|
|
11
17
|
const { workspaceRoot, providers } = options;
|
|
12
18
|
const config = await loadConfig(workspaceRoot, { modelProviders: Object.keys(providers.models) });
|
|
19
|
+
if (!config.model) {
|
|
20
|
+
throw new OpenshainError("config", "this needs a model: add a model section to openshain.yaml (only the interactive CLI needs one)");
|
|
21
|
+
}
|
|
13
22
|
const modelFactory = Object.hasOwn(providers.models, config.model.provider)
|
|
14
23
|
? providers.models[config.model.provider]
|
|
15
24
|
: undefined;
|
|
@@ -22,6 +31,7 @@ export async function createRuntime(options) {
|
|
|
22
31
|
throw new OpenshainError("config", `model ${description.provider}/${description.model} cannot call tools; openshain needs a model with tool support`);
|
|
23
32
|
}
|
|
24
33
|
const registry = await createToolRegistry(workspaceRoot, config, providers.tools);
|
|
34
|
+
const authority = await loadAuthority(workspaceRoot);
|
|
25
35
|
const works = new WorkStore(workspaceRoot);
|
|
26
36
|
return {
|
|
27
37
|
workspaceRoot,
|
|
@@ -31,13 +41,15 @@ export async function createRuntime(options) {
|
|
|
31
41
|
tools: {
|
|
32
42
|
list: () => registry.list().map(({ definition, providerId }) => ({ definition, providerId })),
|
|
33
43
|
hidden: () => registry.hiddenTools(),
|
|
34
|
-
call: createToolCaller({ registry, config, workspaceRoot }),
|
|
44
|
+
call: createToolCaller({ registry, config, workspaceRoot, authority }),
|
|
35
45
|
},
|
|
36
46
|
};
|
|
37
47
|
}
|
|
38
48
|
/** The tool call pipeline on its own: authorize, validate, run, record. For callers that need no model, such as the MCP server. */
|
|
39
49
|
export function createToolCaller(input) {
|
|
40
|
-
|
|
50
|
+
const given = input.authority;
|
|
51
|
+
const current = typeof given === "function" ? given : () => given ?? OPEN_AUTHORITY;
|
|
52
|
+
return (work, call, options) => callTool({ ...input, authority: current(), work, call, ...options });
|
|
41
53
|
}
|
|
42
54
|
/** Registers the tool providers the config names: the caller's factories by id, and modules from the workspace. Needs no model. */
|
|
43
55
|
export async function createToolRegistry(workspaceRoot, config, tools) {
|
|
@@ -58,8 +70,8 @@ export async function createToolRegistry(workspaceRoot, config, tools) {
|
|
|
58
70
|
return registry;
|
|
59
71
|
}
|
|
60
72
|
/**
|
|
61
|
-
* The one place that allows or refuses a call before it runs
|
|
62
|
-
*
|
|
73
|
+
* The one place that allows or refuses a call before it runs by name alone. What the workspace
|
|
74
|
+
* allows a call to do is decided after this, by the policy in `authority/`.
|
|
63
75
|
*/
|
|
64
76
|
function authorize(registry, call) {
|
|
65
77
|
const tool = registry.get(call.name);
|
|
@@ -74,7 +86,7 @@ function authorize(registry, call) {
|
|
|
74
86
|
: { ok: false, code: "unknown_tool", reason: `unknown tool "${call.name}"` };
|
|
75
87
|
}
|
|
76
88
|
async function callTool(input) {
|
|
77
|
-
const { registry, config, workspaceRoot, work, call } = input;
|
|
89
|
+
const { registry, config, workspaceRoot, authority, work, call } = input;
|
|
78
90
|
const reject = async (code, reason) => {
|
|
79
91
|
await work.append({
|
|
80
92
|
type: "tool.rejected",
|
|
@@ -90,6 +102,70 @@ async function callTool(input) {
|
|
|
90
102
|
if (!validation.ok) {
|
|
91
103
|
return reject("schema_mismatch", `input does not match the schema of ${call.name}: ${validation.reason}`);
|
|
92
104
|
}
|
|
105
|
+
// The policy judges after the allow list, unless a person already approved this very call.
|
|
106
|
+
if (input.approvedBy === undefined) {
|
|
107
|
+
const path = pathOf(call.input);
|
|
108
|
+
const judged = evaluate(authority, {
|
|
109
|
+
tool: call.name,
|
|
110
|
+
effect: tool.definition.effect,
|
|
111
|
+
...(path !== undefined && { path }),
|
|
112
|
+
principal: config.principal.id,
|
|
113
|
+
profession: config.profession.id,
|
|
114
|
+
workType: (await work.current()).type,
|
|
115
|
+
businessDate: businessDate(),
|
|
116
|
+
});
|
|
117
|
+
if (judged.kind === "deny")
|
|
118
|
+
return reject("denied", judged.reason);
|
|
119
|
+
if (judged.kind === "approval_required" || judged.kind === "review_required") {
|
|
120
|
+
const review = judged.kind === "review_required";
|
|
121
|
+
const approvalId = `apr_${uuidv7()}`;
|
|
122
|
+
const approvers = judged.rule.approvers ?? [config.principal.id];
|
|
123
|
+
const declared = judged.rule.reviewer;
|
|
124
|
+
const reviewer = declared
|
|
125
|
+
? { role: declared.role, ...(declared.name !== undefined && { name: declared.name }) }
|
|
126
|
+
: undefined;
|
|
127
|
+
await work.append({
|
|
128
|
+
type: "approval.requested",
|
|
129
|
+
payload: {
|
|
130
|
+
approvalId,
|
|
131
|
+
call: { callId: call.id, name: call.name, input: call.input },
|
|
132
|
+
ruleId: judged.rule.id,
|
|
133
|
+
kind: review ? "review" : "approval",
|
|
134
|
+
...(review ? reviewer && { reviewer } : { approvers }),
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
if (review) {
|
|
138
|
+
const built = await reviewPackage(work, {
|
|
139
|
+
approvalId,
|
|
140
|
+
call,
|
|
141
|
+
rule: judged.rule,
|
|
142
|
+
principal: config.principal.id,
|
|
143
|
+
});
|
|
144
|
+
await work.append({ type: "review.requested", payload: { approvalId, package: built } });
|
|
145
|
+
// A copy the person can send to the reviewer, next to the work's own record.
|
|
146
|
+
const dir = join(workspaceRoot, WORK_DIR_NAME, work.id, REVIEW_DIR_NAME);
|
|
147
|
+
await mkdir(dir, { recursive: true });
|
|
148
|
+
await writeFile(join(dir, `${approvalId}.json`), `${JSON.stringify(built, null, 2)}\n`, {
|
|
149
|
+
flag: "wx",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
await work.transition("waiting_approval", `rule ${judged.rule.id} needs ${review ? "a review" : "approval"}`);
|
|
153
|
+
const held = {
|
|
154
|
+
pending: review ? "review" : "approval",
|
|
155
|
+
approval_id: approvalId,
|
|
156
|
+
rule_id: judged.rule.id,
|
|
157
|
+
...(review ? reviewer && { reviewer } : { approvers }),
|
|
158
|
+
...(review && judged.why !== undefined && { why: judged.why }),
|
|
159
|
+
};
|
|
160
|
+
return { content: [{ type: "json", value: held }] };
|
|
161
|
+
}
|
|
162
|
+
if (judged.kind === "allow" && judged.decision) {
|
|
163
|
+
await work.append({
|
|
164
|
+
type: "decision.applied",
|
|
165
|
+
payload: { callId: call.id, decisionId: judged.decision.id },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
93
169
|
await work.append({
|
|
94
170
|
type: "tool.called",
|
|
95
171
|
payload: { callId: call.id, provider: tool.providerId, name: call.name, input: call.input },
|
|
@@ -127,6 +203,72 @@ async function callTool(input) {
|
|
|
127
203
|
});
|
|
128
204
|
return result;
|
|
129
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* What the reviewer is asked to decide on, from the work's own record: the call, the tool calls
|
|
208
|
+
* that came before it, and the agent's last words as the proposal. Sources and company rules stay
|
|
209
|
+
* empty until knowledge is in.
|
|
210
|
+
*/
|
|
211
|
+
async function reviewPackage(work, input) {
|
|
212
|
+
const events = await work.events();
|
|
213
|
+
const facts = [];
|
|
214
|
+
let proposal = "";
|
|
215
|
+
for (const event of events) {
|
|
216
|
+
if (event.type === "tool.called") {
|
|
217
|
+
const { name, input: called } = event.payload;
|
|
218
|
+
const path = pathOf(called);
|
|
219
|
+
facts.push(path === undefined ? name : `${name} ${path}`);
|
|
220
|
+
}
|
|
221
|
+
else if (event.type === "model.completed") {
|
|
222
|
+
const text = event.payload.content
|
|
223
|
+
.filter((part) => part.type === "text")
|
|
224
|
+
.map((part) => part.text)
|
|
225
|
+
.join("\n")
|
|
226
|
+
.trim();
|
|
227
|
+
if (text !== "")
|
|
228
|
+
proposal = text;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const current = await work.current();
|
|
232
|
+
return {
|
|
233
|
+
approvalId: input.approvalId,
|
|
234
|
+
workId: work.id,
|
|
235
|
+
action: {
|
|
236
|
+
name: input.rule.match.action?.toString() ?? input.call.name,
|
|
237
|
+
tool: input.call.name,
|
|
238
|
+
input: input.call.input,
|
|
239
|
+
},
|
|
240
|
+
facts,
|
|
241
|
+
sources: [],
|
|
242
|
+
companyRules: [],
|
|
243
|
+
proposal,
|
|
244
|
+
question: input.rule.reason ??
|
|
245
|
+
`${current.objective} のために ${input.call.name} を実行してよいか、判断をお願いします。`,
|
|
246
|
+
requestedBy: input.principal,
|
|
247
|
+
requestedAt: new Date().toISOString(),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/** The path a call names, normalized to a workspace-relative posix path, when its input has one. */
|
|
251
|
+
function pathOf(input) {
|
|
252
|
+
const path = input?.path;
|
|
253
|
+
if (typeof path !== "string" || path === "")
|
|
254
|
+
return undefined;
|
|
255
|
+
const segments = [];
|
|
256
|
+
for (const segment of path.replaceAll("\\", "/").split("/")) {
|
|
257
|
+
if (segment === "" || segment === ".")
|
|
258
|
+
continue;
|
|
259
|
+
if (segment === "..")
|
|
260
|
+
segments.pop();
|
|
261
|
+
else
|
|
262
|
+
segments.push(segment);
|
|
263
|
+
}
|
|
264
|
+
return segments.join("/");
|
|
265
|
+
}
|
|
266
|
+
/** Today's date on this machine's clock, YYYY-MM-DD. */
|
|
267
|
+
function businessDate() {
|
|
268
|
+
const now = new Date();
|
|
269
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
270
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
271
|
+
}
|
|
130
272
|
function isRejectionCode(code) {
|
|
131
273
|
return TOOL_REJECTION_CODES.includes(code);
|
|
132
274
|
}
|
package/dist/schemas.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JsonSchema } from "./tool/types.ts";
|
|
2
|
-
export type SchemaName = "config.v1" | "events.v1" | "work.v1";
|
|
2
|
+
export type SchemaName = "config.v1" | "events.v1" | "work.v1" | "authority-policy.v1" | "authority-delegations.v1";
|
|
3
3
|
/**
|
|
4
4
|
* The JSON Schemas (draft 2020-12) of the files openshain reads and writes, derived from the zod
|
|
5
5
|
* schemas that validate them. `spec/schemas/` holds this output; `bun run schemas` regenerates it.
|
package/dist/schemas.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { DelegationsFileSchema, PolicyFileSchema } from "./authority/policy.js";
|
|
2
3
|
import { ConfigFileSchema } from "./config/schema.js";
|
|
3
4
|
import { EventFileSchema, payloadFileSchemas } from "./work/events.js";
|
|
4
5
|
import { WorkFileSchema } from "./work/work.js";
|
|
@@ -13,6 +14,8 @@ export function jsonSchemas() {
|
|
|
13
14
|
"config.v1": describe(ConfigFileSchema, "openshain.yaml", "The company workspace manifest as written on disk."),
|
|
14
15
|
"events.v1": eventsSchema(),
|
|
15
16
|
"work.v1": describe(WorkFileSchema, "work.json", "The state of a work as projected from its event log. Never the source of truth."),
|
|
17
|
+
"authority-policy.v1": describe(PolicyFileSchema, "authority/policy.yaml", "The rules that judge tool calls: the first matching rule decides, else the default."),
|
|
18
|
+
"authority-delegations.v1": describe(DelegationsFileSchema, "authority/delegations.yaml", "Who the agent may act for, as which profession, and when."),
|
|
16
19
|
};
|
|
17
20
|
}
|
|
18
21
|
/**
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ToolDefinition } from "./types.ts";
|
|
2
|
+
/** The provider id the runtime records for the tools it runs itself. */
|
|
3
|
+
export declare const RUNTIME_PROVIDER_ID = "runtime";
|
|
4
|
+
/** The one tool the runtime itself provides: stop and ask the person. */
|
|
5
|
+
export declare const ASK_USER: Readonly<ToolDefinition>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ASK_USER_TOOL_NAME } from "./types.js";
|
|
2
|
+
/** The provider id the runtime records for the tools it runs itself. */
|
|
3
|
+
export const RUNTIME_PROVIDER_ID = "runtime";
|
|
4
|
+
/** The one tool the runtime itself provides: stop and ask the person. */
|
|
5
|
+
export const ASK_USER = Object.freeze({
|
|
6
|
+
name: ASK_USER_TOOL_NAME,
|
|
7
|
+
description: "Ask the person you work for a question when you cannot proceed without their answer. Use it sparingly; prefer the workspace over guessing. The work waits until the answer is recorded with work_answer.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
question: {
|
|
12
|
+
type: "string",
|
|
13
|
+
minLength: 1,
|
|
14
|
+
maxLength: 10_000,
|
|
15
|
+
description: "The question, in the person's language.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ["question"],
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
},
|
|
21
|
+
effect: "observe",
|
|
22
|
+
});
|
package/dist/tool/paths.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Paths the runtime keeps for itself. Tools may not read or write them. */
|
|
2
|
-
export declare const RESERVED_PATHS: readonly ["openshain.yaml", "work"];
|
|
2
|
+
export declare const RESERVED_PATHS: readonly ["openshain.yaml", "work", "principals", "authority"];
|
|
3
3
|
/**
|
|
4
4
|
* Turns a tool-supplied relative path into an absolute path inside the workspace.
|
|
5
5
|
*
|
package/dist/tool/paths.js
CHANGED
|
@@ -2,7 +2,7 @@ import { lstat, readlink, realpath } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { OpenshainError } from "../errors.js";
|
|
4
4
|
/** Paths the runtime keeps for itself. Tools may not read or write them. */
|
|
5
|
-
export const RESERVED_PATHS = ["openshain.yaml", "work"];
|
|
5
|
+
export const RESERVED_PATHS = ["openshain.yaml", "work", "principals", "authority"];
|
|
6
6
|
const MAX_SYMLINK_HOPS = 32;
|
|
7
7
|
/**
|
|
8
8
|
* Turns a tool-supplied relative path into an absolute path inside the workspace.
|
package/dist/tool/types.js
CHANGED
package/dist/work/events.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface ModelUsage {
|
|
|
43
43
|
/** The part of outputTokens spent on reasoning. */
|
|
44
44
|
reasoningTokens?: number;
|
|
45
45
|
}
|
|
46
|
-
export declare const TOOL_REJECTION_CODES: readonly ["schema_mismatch", "unknown_tool", "not_allowed", "reserved_path", "outside_workspace", "invalid_path"];
|
|
46
|
+
export declare const TOOL_REJECTION_CODES: readonly ["schema_mismatch", "unknown_tool", "not_allowed", "reserved_path", "outside_workspace", "invalid_path", "limit_reached", "denied", "rejected_by_person"];
|
|
47
47
|
export type ToolRejectionCode = (typeof TOOL_REJECTION_CODES)[number];
|
|
48
48
|
export interface EventPayloads {
|
|
49
49
|
"work.created": {
|
|
@@ -106,10 +106,55 @@ export interface EventPayloads {
|
|
|
106
106
|
callId: string;
|
|
107
107
|
answer: string;
|
|
108
108
|
};
|
|
109
|
+
/** A tool call the policy holds for a person's or a reviewer's approval. The work waits. */
|
|
110
|
+
"approval.requested": {
|
|
111
|
+
approvalId: string;
|
|
112
|
+
call: {
|
|
113
|
+
callId: string;
|
|
114
|
+
name: string;
|
|
115
|
+
input: unknown;
|
|
116
|
+
};
|
|
117
|
+
ruleId: string;
|
|
118
|
+
kind: "approval" | "review";
|
|
119
|
+
approvers?: string[];
|
|
120
|
+
reviewer?: {
|
|
121
|
+
role: string;
|
|
122
|
+
name?: string;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
/** The package handed to the reviewer: the call, what the work established, and the question. */
|
|
126
|
+
"review.requested": {
|
|
127
|
+
approvalId: string;
|
|
128
|
+
package: ReviewPackage;
|
|
129
|
+
};
|
|
130
|
+
/** The reviewer's answer. A decision id when they wrote one; absent when they refused. */
|
|
131
|
+
"review.decided": {
|
|
132
|
+
approvalId: string;
|
|
133
|
+
decisionId?: string;
|
|
134
|
+
};
|
|
135
|
+
/** A call that ran because an approved decision covers it. */
|
|
136
|
+
"decision.applied": {
|
|
137
|
+
callId: string;
|
|
138
|
+
decisionId: string;
|
|
139
|
+
};
|
|
140
|
+
/** The answer to an approval: approve runs the call, reject refuses it, modify runs it with the reviewer's input. */
|
|
141
|
+
"approval.decided": {
|
|
142
|
+
approvalId: string;
|
|
143
|
+
decision: "approve" | "reject" | "modify";
|
|
144
|
+
by: string;
|
|
145
|
+
comment?: string;
|
|
146
|
+
modifiedInput?: unknown;
|
|
147
|
+
};
|
|
109
148
|
/** What the person said in a session. Becomes a user message in the projection. */
|
|
110
149
|
"human.message": {
|
|
111
150
|
text: string;
|
|
112
151
|
};
|
|
152
|
+
/** A prompt command expanded for the model: its name, where it came from, and the text handed over. */
|
|
153
|
+
"prompt.expanded": {
|
|
154
|
+
name: string;
|
|
155
|
+
source: string;
|
|
156
|
+
text: string;
|
|
157
|
+
};
|
|
113
158
|
"usage.recorded": {
|
|
114
159
|
kind: "model_inference";
|
|
115
160
|
provider: string;
|
|
@@ -135,6 +180,33 @@ export interface EventPayloads {
|
|
|
135
180
|
detail: string;
|
|
136
181
|
};
|
|
137
182
|
}
|
|
183
|
+
/** What a reviewer is asked to decide on, built from the work's own record. */
|
|
184
|
+
export interface ReviewPackage {
|
|
185
|
+
approvalId: string;
|
|
186
|
+
workId: string;
|
|
187
|
+
action: {
|
|
188
|
+
name: string;
|
|
189
|
+
tool: string;
|
|
190
|
+
input: unknown;
|
|
191
|
+
};
|
|
192
|
+
/** What the work established before this call: the tool calls it made. */
|
|
193
|
+
facts: string[];
|
|
194
|
+
/** Sources and company rules the work cited. Empty until knowledge is in. */
|
|
195
|
+
sources: {
|
|
196
|
+
id: string;
|
|
197
|
+
locator?: string;
|
|
198
|
+
version?: string;
|
|
199
|
+
}[];
|
|
200
|
+
companyRules: {
|
|
201
|
+
id: string;
|
|
202
|
+
statement: string;
|
|
203
|
+
}[];
|
|
204
|
+
/** What the agent proposes, in its own words. */
|
|
205
|
+
proposal: string;
|
|
206
|
+
question: string;
|
|
207
|
+
requestedBy: string;
|
|
208
|
+
requestedAt: string;
|
|
209
|
+
}
|
|
138
210
|
export type EventType = keyof EventPayloads;
|
|
139
211
|
interface Envelope {
|
|
140
212
|
v: 1;
|
|
@@ -232,9 +304,12 @@ export declare const payloadFileSchemas: {
|
|
|
232
304
|
call_id: z.ZodString;
|
|
233
305
|
name: z.ZodString;
|
|
234
306
|
code: z.ZodEnum<{
|
|
307
|
+
denied: "denied";
|
|
235
308
|
invalid_path: "invalid_path";
|
|
309
|
+
limit_reached: "limit_reached";
|
|
236
310
|
not_allowed: "not_allowed";
|
|
237
311
|
outside_workspace: "outside_workspace";
|
|
312
|
+
rejected_by_person: "rejected_by_person";
|
|
238
313
|
reserved_path: "reserved_path";
|
|
239
314
|
schema_mismatch: "schema_mismatch";
|
|
240
315
|
unknown_tool: "unknown_tool";
|
|
@@ -249,9 +324,77 @@ export declare const payloadFileSchemas: {
|
|
|
249
324
|
call_id: z.ZodString;
|
|
250
325
|
answer: z.ZodString;
|
|
251
326
|
}, z.core.$loose>;
|
|
327
|
+
"approval.requested": z.ZodObject<{
|
|
328
|
+
approval_id: z.ZodString;
|
|
329
|
+
call: z.ZodObject<{
|
|
330
|
+
call_id: z.ZodString;
|
|
331
|
+
name: z.ZodString;
|
|
332
|
+
input: z.ZodUnknown;
|
|
333
|
+
}, z.core.$loose>;
|
|
334
|
+
rule_id: z.ZodString;
|
|
335
|
+
kind: z.ZodEnum<{
|
|
336
|
+
approval: "approval";
|
|
337
|
+
review: "review";
|
|
338
|
+
}>;
|
|
339
|
+
approvers: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
340
|
+
reviewer: z.ZodOptional<z.ZodObject<{
|
|
341
|
+
role: z.ZodString;
|
|
342
|
+
name: z.ZodOptional<z.ZodString>;
|
|
343
|
+
}, z.core.$loose>>;
|
|
344
|
+
}, z.core.$loose>;
|
|
345
|
+
"review.requested": z.ZodObject<{
|
|
346
|
+
approval_id: z.ZodString;
|
|
347
|
+
package: z.ZodObject<{
|
|
348
|
+
approval_id: z.ZodString;
|
|
349
|
+
work_id: z.ZodString;
|
|
350
|
+
action: z.ZodObject<{
|
|
351
|
+
name: z.ZodString;
|
|
352
|
+
tool: z.ZodString;
|
|
353
|
+
input: z.ZodUnknown;
|
|
354
|
+
}, z.core.$loose>;
|
|
355
|
+
facts: z.ZodArray<z.ZodString>;
|
|
356
|
+
sources: z.ZodArray<z.ZodObject<{
|
|
357
|
+
id: z.ZodString;
|
|
358
|
+
locator: z.ZodOptional<z.ZodString>;
|
|
359
|
+
version: z.ZodOptional<z.ZodString>;
|
|
360
|
+
}, z.core.$loose>>;
|
|
361
|
+
company_rules: z.ZodArray<z.ZodObject<{
|
|
362
|
+
id: z.ZodString;
|
|
363
|
+
statement: z.ZodString;
|
|
364
|
+
}, z.core.$loose>>;
|
|
365
|
+
proposal: z.ZodString;
|
|
366
|
+
question: z.ZodString;
|
|
367
|
+
requested_by: z.ZodString;
|
|
368
|
+
requested_at: z.ZodISODateTime;
|
|
369
|
+
}, z.core.$loose>;
|
|
370
|
+
}, z.core.$loose>;
|
|
371
|
+
"review.decided": z.ZodObject<{
|
|
372
|
+
approval_id: z.ZodString;
|
|
373
|
+
decision_id: z.ZodOptional<z.ZodString>;
|
|
374
|
+
}, z.core.$loose>;
|
|
375
|
+
"decision.applied": z.ZodObject<{
|
|
376
|
+
call_id: z.ZodString;
|
|
377
|
+
decision_id: z.ZodString;
|
|
378
|
+
}, z.core.$loose>;
|
|
379
|
+
"approval.decided": z.ZodObject<{
|
|
380
|
+
approval_id: z.ZodString;
|
|
381
|
+
decision: z.ZodEnum<{
|
|
382
|
+
approve: "approve";
|
|
383
|
+
modify: "modify";
|
|
384
|
+
reject: "reject";
|
|
385
|
+
}>;
|
|
386
|
+
by: z.ZodString;
|
|
387
|
+
comment: z.ZodOptional<z.ZodString>;
|
|
388
|
+
modified_input: z.ZodOptional<z.ZodUnknown>;
|
|
389
|
+
}, z.core.$loose>;
|
|
252
390
|
"human.message": z.ZodObject<{
|
|
253
391
|
text: z.ZodString;
|
|
254
392
|
}, z.core.$loose>;
|
|
393
|
+
"prompt.expanded": z.ZodObject<{
|
|
394
|
+
name: z.ZodString;
|
|
395
|
+
source: z.ZodString;
|
|
396
|
+
text: z.ZodString;
|
|
397
|
+
}, z.core.$loose>;
|
|
255
398
|
"usage.recorded": z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
256
399
|
kind: z.ZodLiteral<"model_inference">;
|
|
257
400
|
provider: z.ZodString;
|
|
@@ -308,4 +451,10 @@ export declare function eventToFile(event: AnyEvent): EventFile;
|
|
|
308
451
|
*/
|
|
309
452
|
export declare function canonical(value: unknown, insideData?: boolean, seen?: WeakSet<object>): unknown;
|
|
310
453
|
export declare function eventFromFile(input: unknown): AnyEvent;
|
|
454
|
+
/**
|
|
455
|
+
* Validates a payload given in the file form (snake_case, as in spec/schemas/events.v1.json) and
|
|
456
|
+
* returns it in the in-memory form. For events a client hands the runtime to record.
|
|
457
|
+
*/
|
|
458
|
+
export declare function parsePayloadFile<T extends EventType>(type: T, payload: unknown): EventPayloads[T];
|
|
459
|
+
export declare function isKnownEventType(type: string): type is EventType;
|
|
311
460
|
export {};
|