openings 0.1.55 → 0.1.56

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openings",
3
- "version": "0.1.55",
3
+ "version": "0.1.56",
4
4
  "description": "A free, candidate-safe job-search substrate for AI agents",
5
5
  "license": "MIT",
6
6
  "repository": { "type": "git", "url": "git+https://github.com/abhay-avagama/hiring-agent.git" },
package/src/index.ts CHANGED
@@ -40,7 +40,7 @@ export { createJobCoverageReader, projectJobCoverage } from "./job-coverage.ts";
40
40
  export type { CountryJobCoverage, JobCoverageSummary } from "./job-coverage.ts";
41
41
  export { createHostedRuntime } from "./runtime.ts";
42
42
  export type { SnapshotStore } from "./crawler.ts";
43
- export { createToolHandler } from "./tools.ts";
43
+ export { createToolHandler, SCOPES, scopeForTool, type Scope } from "./tools.ts";
44
44
  export { createMcpHandler, FLOW_INSTRUCTIONS } from "./mcp.ts";
45
45
  export type { UpdateNotice } from "./update-check.ts";
46
46
  export { VERSION } from "./version.ts";
package/src/tools.ts CHANGED
@@ -33,7 +33,35 @@ interface JobWorkflows {
33
33
  optimizeResume(input: unknown): Promise<OptimizeResumeResult>;
34
34
  }
35
35
 
36
- export function createToolHandler(catalog: Catalog, workflows: JobWorkflows, options: { onCall?(name: string, input: Record<string, unknown>, result: unknown): void } = {}) {
36
+ /**
37
+ * What a token is allowed to reach. The split is on what the tool touches, not on what it changes: four tools read
38
+ * job data and nothing else, three parse a candidate's resume. A client that only wants job search should be able
39
+ * to say so and be held to it, which is the promise we already make in prose and could not enforce.
40
+ */
41
+ export const SCOPES = { jobs: "jobs.read", resume: "resume.analyze" } as const;
42
+ export type Scope = (typeof SCOPES)[keyof typeof SCOPES];
43
+
44
+ const TOOL_SCOPE: Record<ToolDefinition["name"], Scope> = {
45
+ prepare_job_search: SCOPES.jobs, get_job_coverage: SCOPES.jobs, search_jobs: SCOPES.jobs, get_job: SCOPES.jobs,
46
+ recommend_jobs: SCOPES.resume, analyze_job_fit: SCOPES.resume, optimize_resume: SCOPES.resume,
47
+ };
48
+
49
+ /** The scope a tool needs, for a caller that wants to explain the boundary rather than discover it by refusal. */
50
+ export function scopeForTool(name: string): Scope | undefined {
51
+ return TOOL_SCOPE[name as ToolDefinition["name"]];
52
+ }
53
+
54
+ export function createToolHandler(catalog: Catalog, workflows: JobWorkflows, options: {
55
+ onCall?(name: string, input: Record<string, unknown>, result: unknown): void;
56
+ /**
57
+ * Scopes this token was granted. Undefined means every tool, which is deliberate: tokens issued before the
58
+ * split carry no scope claim, and refusing them would break every client that already works. Only a token
59
+ * that states its scopes is held to them.
60
+ */
61
+ scopes?: readonly string[];
62
+ } = {}) {
63
+ const granted = options.scopes === undefined ? undefined : new Set(options.scopes);
64
+ const allowed = (name: ToolDefinition["name"]) => granted === undefined || granted.has(TOOL_SCOPE[name]);
37
65
  const definitions: Array<Omit<ToolDefinition, "annotations">> = [
38
66
  {
39
67
  name: "prepare_job_search",
@@ -209,8 +237,14 @@ export function createToolHandler(catalog: Catalog, workflows: JobWorkflows, opt
209
237
  ];
210
238
 
211
239
  return {
212
- list: () => definitions.map((definition) => ({ ...definition, annotations: annotationsFor(definition.name) })),
240
+ // A client sees only what it may call. Listing a tool it would be refused teaches it to try and fail.
241
+ list: () => definitions.filter((definition) => allowed(definition.name))
242
+ .map((definition) => ({ ...definition, annotations: annotationsFor(definition.name) })),
213
243
  async call(name: string, input: Record<string, unknown>) {
244
+ const needed = TOOL_SCOPE[name as ToolDefinition["name"]];
245
+ if (needed && !allowed(name as ToolDefinition["name"])) {
246
+ throw new Error(`${name} needs the ${needed} scope, which this connection was not granted. Reconnect and allow it.`);
247
+ }
214
248
  const result = await dispatch(name, input);
215
249
  try { options.onCall?.(name, input, result); } catch { /* usage reporting never affects a tool result */ }
216
250
  return result;
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.1.55";
1
+ export const VERSION = "0.1.56";